Skip to content

Commit 6cfabdf

Browse files
committed
Initial commit
0 parents  commit 6cfabdf

File tree

8 files changed

+261
-0
lines changed

8 files changed

+261
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/.idea
2+
/bin
3+
/sandbox
4+
._*
5+
.DS_Store

Makefile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
BASE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
2+
3+
.PHONY: all
4+
all:
5+
$(MAKE) -C $(dir (BASE_DIR)) build
6+
7+
.PHOHNY: build
8+
build: bin/webdav-server
9+
10+
.PHONY: clean
11+
clean:
12+
@rm -rf bin sandbox
13+
go clean -cache -testcache -r
14+
15+
sandbox:
16+
@mkdir -p sandbox
17+
18+
bin/webdav-server: sandbox cmd/**/*.go pkg/**/*.go
19+
CGO_ENABLED=0 \
20+
go build -a -ldflags '-extldflags "-static"' -tags netgo -installsuffix netgo \
21+
-o bin/webdav-server ./cmd/webdav
22+
23+
.PHONY:
24+
run: build
25+
bin/webdav-server

cmd/webdav/main.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"os"
6+
"os/signal"
7+
"syscall"
8+
9+
"github.com/craigcondit/webdav-server/pkg/webdav"
10+
"gopkg.in/yaml.v3"
11+
)
12+
13+
func main() {
14+
contentRoot, ok := os.LookupEnv("CONTENT_ROOT")
15+
if !ok {
16+
contentRoot = "./sandbox"
17+
}
18+
19+
listenAddr, ok := os.LookupEnv("LISTEN_ADDR")
20+
if !ok {
21+
listenAddr = ":8080"
22+
}
23+
24+
usersFile, ok := os.LookupEnv("USERS_FILE")
25+
if !ok {
26+
usersFile = "./conf/users.yaml"
27+
}
28+
users := make(map[string]string)
29+
data, err := os.ReadFile(usersFile)
30+
if err != nil {
31+
log.Fatal(err)
32+
}
33+
if err = yaml.Unmarshal(data, &users); err != nil {
34+
log.Fatal(err)
35+
}
36+
37+
server := webdav.NewWebDavServer(contentRoot, listenAddr, users)
38+
server.Start()
39+
40+
done := make(chan struct{})
41+
go func() {
42+
c := make(chan os.Signal, 1)
43+
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
44+
<-c
45+
close(done)
46+
}()
47+
<-done
48+
server.Stop()
49+
}

conf/users.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
# test:test
3+
test: "$apr1$nw/o4A7S$qmck7MVFLZoWJ/9H6ZVm2/"

go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module github.com/craigcondit/webdav-server
2+
3+
go 1.20
4+
5+
require (
6+
github.com/tg123/go-htpasswd v1.2.1
7+
golang.org/x/net v0.8.0
8+
gopkg.in/yaml.v3 v3.0.1
9+
)
10+
11+
require (
12+
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
13+
golang.org/x/crypto v0.6.0 // indirect
14+
)

go.sum

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
2+
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
3+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
5+
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
6+
github.com/tg123/go-htpasswd v1.2.1 h1:i4wfsX1KvvkyoMiHZzjS0VzbAPWfxzI8INcZAKtutoU=
7+
github.com/tg123/go-htpasswd v1.2.1/go.mod h1:erHp1B86KXdwQf1X5ZrLb7erXZnWueEQezb2dql4q58=
8+
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
9+
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
10+
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
11+
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
12+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
13+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
14+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
15+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

pkg/webdav/auth.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package webdav
2+
3+
import (
4+
"encoding/base64"
5+
"log"
6+
"net/http"
7+
"strings"
8+
9+
"github.com/tg123/go-htpasswd"
10+
)
11+
12+
type BasicAuthenticator struct {
13+
auth *htpasswd.File
14+
nextHandler http.Handler
15+
}
16+
17+
var _ http.Handler = &BasicAuthenticator{}
18+
19+
func NewBasicAuthenticator(auth *htpasswd.File, nextHandler http.Handler) *BasicAuthenticator {
20+
return &BasicAuthenticator{
21+
auth: auth,
22+
nextHandler: nextHandler,
23+
}
24+
}
25+
26+
func (b BasicAuthenticator) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
27+
user, ok := b.authenticate(writer, request)
28+
if !ok {
29+
return
30+
}
31+
request.Header.Add("X-Remote-User", user)
32+
b.nextHandler.ServeHTTP(writer, request)
33+
}
34+
35+
func (b BasicAuthenticator) authenticate(writer http.ResponseWriter, request *http.Request) (string, bool) {
36+
// don't authenticate OPTIONS calls
37+
if strings.ToLower(request.Method) == "options" {
38+
return "", true
39+
}
40+
41+
s := strings.SplitN(request.Header.Get("Authorization"), " ", 2)
42+
if len(s) != 2 {
43+
// unauthenticated
44+
b.challenge(writer, request)
45+
return "", false
46+
}
47+
b64, err := base64.StdEncoding.DecodeString(s[1])
48+
if err != nil {
49+
b.challenge(writer, request)
50+
return "", false
51+
}
52+
pair := strings.SplitN(string(b64), ":", 2)
53+
if len(pair) != 2 {
54+
b.challenge(writer, request)
55+
return "", false
56+
}
57+
58+
if !b.auth.Match(pair[0], pair[1]) {
59+
b.challenge(writer, request)
60+
return "", false
61+
}
62+
63+
return pair[0], true
64+
}
65+
66+
func (b BasicAuthenticator) challenge(writer http.ResponseWriter, request *http.Request) {
67+
writer.Header().Set("WWW-Authenticate", `Basic realm="webdav"`)
68+
writer.WriteHeader(401)
69+
writer.Write([]byte("401 Unauthorized\n"))
70+
remoteAddr := request.RemoteAddr
71+
if remoteAddr == "" {
72+
remoteAddr = "-"
73+
}
74+
log.Default().Printf("%s - %s %v\n", remoteAddr, request.Method, request.URL)
75+
}

pkg/webdav/webdav_server.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package webdav
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"log"
8+
"net/http"
9+
"strings"
10+
"time"
11+
12+
"github.com/tg123/go-htpasswd"
13+
"golang.org/x/net/webdav"
14+
)
15+
16+
type WebDavServer struct {
17+
server *http.Server
18+
contentRoot string
19+
}
20+
21+
func NewWebDavServer(contentRoot string, listenAddr string, users map[string]string) *WebDavServer {
22+
handler := &webdav.Handler{
23+
FileSystem: webdav.Dir(contentRoot),
24+
LockSystem: webdav.NewMemLS(),
25+
Logger: func(r *http.Request, err error) {
26+
remoteAddr := r.RemoteAddr
27+
if remoteAddr == "" {
28+
remoteAddr = "-"
29+
}
30+
remoteUser := r.Header.Get("X-Remote-User")
31+
if remoteUser == "" {
32+
remoteUser = "-"
33+
}
34+
log.Default().Printf("%s %s %s %v\n", remoteAddr, remoteUser, r.Method, r.URL)
35+
},
36+
}
37+
htPasswdFile := ""
38+
for user, pw := range users {
39+
htPasswdFile = htPasswdFile + fmt.Sprintf("%s:%s\n", user, pw)
40+
}
41+
auth, err := htpasswd.NewFromReader(strings.NewReader(htPasswdFile), htpasswd.DefaultSystems, nil)
42+
if err != nil {
43+
log.Fatal(err)
44+
}
45+
basicAuth := NewBasicAuthenticator(auth, handler)
46+
mux := http.NewServeMux()
47+
mux.Handle("/", basicAuth)
48+
server := &http.Server{
49+
Addr: listenAddr,
50+
Handler: mux,
51+
ReadHeaderTimeout: 10 * time.Second,
52+
}
53+
return &WebDavServer{
54+
server: server,
55+
contentRoot: contentRoot,
56+
}
57+
}
58+
59+
func (s *WebDavServer) Start() {
60+
log.Default().Printf("Starting WebDAV server on %s using content root '%s'.\n", s.server.Addr, s.contentRoot)
61+
go func() {
62+
if err := s.server.ListenAndServe(); err != nil {
63+
if !errors.Is(err, http.ErrServerClosed) {
64+
log.Fatal(err)
65+
}
66+
}
67+
}()
68+
}
69+
70+
func (s *WebDavServer) Stop() {
71+
s.server.Close()
72+
if err := s.server.Shutdown(context.Background()); err != nil {
73+
log.Fatal(err)
74+
}
75+
}

0 commit comments

Comments
 (0)