aboutsummaryrefslogtreecommitdiff
path: root/wfm.go
blob: 07628ea2f41e53a2d31df9d9736572029126596a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
// Web File Manager

package main

import (
	"crypto/tls"
	"flag"
	"log"
	"net"
	"net/http"
	"os"
	"os/user"
	"strconv"
	"strings"
	"syscall"

	_ "github.com/breml/rootcerts"
	"github.com/juju/ratelimit"
	"golang.org/x/crypto/acme/autocert"
)

type multiString []string

var (
	vers       = "2.0.4"
	bindProto  = flag.String("proto", "tcp", "tcp, tcp4, tcp6, etc")
	bindAddr   = flag.String("addr", "127.0.0.1:8080", "Listen address, eg: :443")
	bindExtra  = flag.String("addr_extra", "", "Extra non-TLS listener address, eg: :8081")
	chrootDir  = flag.String("chroot", "", "Directory to chroot to")
	suidUser   = flag.String("setuid", "", "Username to setuid to")
	allowRoot  = flag.Bool("allow_root", false, "allow to run as uid=0/root without setuid")
	logFile    = flag.String("logfile", "", "Log file name (default stdout)")
	passwdDb   = flag.String("passwd", "", "wfm password file, eg: /usr/local/etc/wfmpw.json")
	noPwdDbRW  = flag.Bool("nopass_rw", false, "allow read-write access if there is no password file")
	aboutRnt   = flag.Bool("about_runtime", true, "Display runtime info in About Dialog")
	showDot    = flag.Bool("show_dot", false, "show dot files and folders")
	listArc    = flag.Bool("list_archive_contents", false, "list contents of archives (expensive!)")
	rateLim    = flag.Int("rate_limit", 0, "rate limit for upload/download in MB/s, 0 no limit")
	wfmPfx     = flag.String("prefix", "/", "Default url prefix for WFM access")
	docSrv     = flag.String("doc_srv", "", "Serve regular http files, fsdir:prefix, eg /var/www/:/home/")
	cacheCtl   = flag.String("cache_ctl", "no-cache", "HTTP Header Cache Control")
	robots     = flag.Bool("robots", false, "allow robots")
	acmDir     = flag.String("acm_dir", "", "autocert cache, eg: /var/cache (inside chroot)")
	acmBind    = flag.String("acm_addr", "", "autocert manager listen address, eg: :80")
	acmWhlist  multiString // this flag set in main
	f2bEnabled = flag.Bool("f2b", true, "ban ip addresses on user/pass failures")
	f2bDump    = flag.String("f2b_dump", "", "enable f2b dump at this prefix, eg. /f2bdump (default no)")
)

func userId(usr string) (int, int, error) {
	u, err := user.Lookup(usr)
	if err != nil {
		return 0, 0, err
	}
	ui, err := strconv.Atoi(u.Uid)
	if err != nil {
		return 0, 0, err
	}
	gi, err := strconv.Atoi(u.Gid)
	if err != nil {
		return 0, 0, err
	}
	return ui, gi, nil
}

func setUid(ui, gi int) error {
	if ui == 0 || gi == 0 {
		return nil
	}
	err := syscall.Setgid(gi)
	if err != nil {
		return err
	}
	err = syscall.Setuid(ui)
	if err != nil {
		return err
	}
	return nil
}

func (z *multiString) String() string {
	return "something"
}

func (z *multiString) Set(v string) error {
	*z = append(*z, v)
	return nil
}

func main() {
	var err error
	flag.Var(&acmWhlist, "acm_host", "autocert manager allowed hostname (multi)")
	flag.Parse()

	if flag.Arg(0) == "user" {
		manageUsers()
		return
	}

	log.Print("WFM Starting up")

	if *passwdDb != "" {
		loadUsers()
	}

	if *logFile != "" {
		lf, err := os.OpenFile(*logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
		if err != nil {
			log.Fatal(err)
		}
		defer lf.Close()
		log.SetOutput(lf)
	}

	// find uid/gid for setuid before chroot
	var suid, sgid int
	if *suidUser != "" {
		suid, sgid, err = userId(*suidUser)
		if err != nil {
			log.Fatal("unable to find setuid user", err)
		}
	}

	// run autocert manager before chroot/setuid
	// however it doesn't matter for chroot as certs will land in chroot *adir anyway
	acm := autocert.Manager{}
	if *bindAddr != "" && *acmDir != "" && len(acmWhlist) > 0 {
		acm.Prompt = autocert.AcceptTOS
		acm.Cache = autocert.DirCache(*acmDir)
		acm.HostPolicy = autocert.HostWhitelist(acmWhlist...)
		go http.ListenAndServe(*acmBind, acm.HTTPHandler(nil))
		log.Printf("Autocert enabled for %v", acmWhlist)
	}

	// chroot now
	if *chrootDir != "" {
		err := syscall.Chroot(*chrootDir)
		if err != nil {
			log.Fatal("chroot", err)
		}
		log.Printf("Chroot to %q", *chrootDir)
	}

	// listen/bind to port before setuid
	l, err := net.Listen(*bindProto, *bindAddr)
	if err != nil {
		log.Fatalf("unable to listen on %v: %v", *bindAddr, err)
	}
	log.Printf("Listening on %q", *bindAddr)

	// setuid now
	err = setUid(suid, sgid)
	if err != nil {
		log.Fatalf("unable to suid for %v: %v", *suidUser, err)
	}
	if !*allowRoot && os.Getuid() == 0 {
		log.Fatal("you probably dont want to run wfm as root, use --allow_root flag to force it")
	}
	log.Printf("Setuid UID=%d GID=%d", os.Geteuid(), os.Getgid())

	// rate limit setup
	if *rateLim != 0 {
		rlBu = ratelimit.NewBucketWithRate(float64(*rateLim<<20), 1<<10)
	}

	// http stuff
	mux := http.NewServeMux()
	mux.HandleFunc(*wfmPfx, wfmMain)
	mux.HandleFunc("/favicon.ico", dispFavIcon)
	mux.HandleFunc("/robots.txt", dispRobots)
	if *f2bDump != "" {
		mux.HandleFunc(*f2bDump, dumpf2b)
	}
	if *docSrv != "" {
		ds := strings.Split(*docSrv, ":")
		log.Printf("Starting doc handler for dir %v at %v", ds[0], ds[1])
		mux.Handle(ds[1], http.StripPrefix(ds[1], http.FileServer(http.Dir(ds[0]))))
	}

	if *bindExtra != "" {
		log.Printf("Listening (extra) on %q", *bindAddr)
		go http.ListenAndServe(*bindExtra, mux)
	}
	if *bindAddr != "" && *acmDir != "" && len(acmWhlist) > 0 {
		https := &http.Server{
			Addr:      *bindAddr,
			Handler:   mux,
			TLSConfig: &tls.Config{GetCertificate: acm.GetCertificate},
		}
		log.Printf("Starting HTTPS TLS Server")
		err = https.ServeTLS(l, "", "")
	} else {
		log.Printf("Starting HTTP Server")
		err = http.Serve(l, mux)
	}
	if err != nil {
		log.Fatal(err)
	}
}