1// Package webdav implements a WebDAV server backed by rclone VFS
2package webdav
3
4import (
5	"context"
6	"net/http"
7	"os"
8	"strings"
9	"time"
10
11	"github.com/rclone/rclone/cmd"
12	"github.com/rclone/rclone/cmd/serve/httplib"
13	"github.com/rclone/rclone/cmd/serve/httplib/httpflags"
14	"github.com/rclone/rclone/cmd/serve/proxy"
15	"github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
16	"github.com/rclone/rclone/fs"
17	"github.com/rclone/rclone/fs/config/flags"
18	"github.com/rclone/rclone/fs/hash"
19	"github.com/rclone/rclone/lib/errors"
20	"github.com/rclone/rclone/lib/http/serve"
21	"github.com/rclone/rclone/vfs"
22	"github.com/rclone/rclone/vfs/vfsflags"
23	"github.com/spf13/cobra"
24	"golang.org/x/net/webdav"
25)
26
27var (
28	hashName      string
29	hashType      = hash.None
30	disableGETDir = false
31)
32
33func init() {
34	flagSet := Command.Flags()
35	httpflags.AddFlags(flagSet)
36	vfsflags.AddFlags(flagSet)
37	proxyflags.AddFlags(flagSet)
38	flags.StringVarP(flagSet, &hashName, "etag-hash", "", "", "Which hash to use for the ETag, or auto or blank for off")
39	flags.BoolVarP(flagSet, &disableGETDir, "disable-dir-list", "", false, "Disable HTML directory list on GET request for a directory")
40}
41
42// Command definition for cobra
43var Command = &cobra.Command{
44	Use:   "webdav remote:path",
45	Short: `Serve remote:path over webdav.`,
46	Long: `
47rclone serve webdav implements a basic webdav server to serve the
48remote over HTTP via the webdav protocol. This can be viewed with a
49webdav client, through a web browser, or you can make a remote of
50type webdav to read and write it.
51
52### Webdav options
53
54#### --etag-hash
55
56This controls the ETag header.  Without this flag the ETag will be
57based on the ModTime and Size of the object.
58
59If this flag is set to "auto" then rclone will choose the first
60supported hash on the backend or you can use a named hash such as
61"MD5" or "SHA-1".
62
63Use "rclone hashsum" to see the full list.
64
65` + httplib.Help + vfs.Help + proxy.Help,
66	RunE: func(command *cobra.Command, args []string) error {
67		var f fs.Fs
68		if proxyflags.Opt.AuthProxy == "" {
69			cmd.CheckArgs(1, 1, command, args)
70			f = cmd.NewFsSrc(args)
71		} else {
72			cmd.CheckArgs(0, 0, command, args)
73		}
74		hashType = hash.None
75		if hashName == "auto" {
76			hashType = f.Hashes().GetOne()
77		} else if hashName != "" {
78			err := hashType.Set(hashName)
79			if err != nil {
80				return err
81			}
82		}
83		if hashType != hash.None {
84			fs.Debugf(f, "Using hash %v for ETag", hashType)
85		}
86		cmd.Run(false, false, command, func() error {
87			s := newWebDAV(context.Background(), f, &httpflags.Opt)
88			err := s.serve()
89			if err != nil {
90				return err
91			}
92			s.Wait()
93			return nil
94		})
95		return nil
96	},
97}
98
99// WebDAV is a webdav.FileSystem interface
100//
101// A FileSystem implements access to a collection of named files. The elements
102// in a file path are separated by slash ('/', U+002F) characters, regardless
103// of host operating system convention.
104//
105// Each method has the same semantics as the os package's function of the same
106// name.
107//
108// Note that the os.Rename documentation says that "OS-specific restrictions
109// might apply". In particular, whether or not renaming a file or directory
110// overwriting another existing file or directory is an error is OS-dependent.
111type WebDAV struct {
112	*httplib.Server
113	f             fs.Fs
114	_vfs          *vfs.VFS // don't use directly, use getVFS
115	webdavhandler *webdav.Handler
116	proxy         *proxy.Proxy
117	ctx           context.Context // for global config
118}
119
120// check interface
121var _ webdav.FileSystem = (*WebDAV)(nil)
122
123// Make a new WebDAV to serve the remote
124func newWebDAV(ctx context.Context, f fs.Fs, opt *httplib.Options) *WebDAV {
125	w := &WebDAV{
126		f:   f,
127		ctx: ctx,
128	}
129	if proxyflags.Opt.AuthProxy != "" {
130		w.proxy = proxy.New(ctx, &proxyflags.Opt)
131		// override auth
132		copyOpt := *opt
133		copyOpt.Auth = w.auth
134		opt = &copyOpt
135	} else {
136		w._vfs = vfs.New(f, &vfsflags.Opt)
137	}
138	w.Server = httplib.NewServer(http.HandlerFunc(w.handler), opt)
139	webdavHandler := &webdav.Handler{
140		Prefix:     w.Server.Opt.BaseURL,
141		FileSystem: w,
142		LockSystem: webdav.NewMemLS(),
143		Logger:     w.logRequest, // FIXME
144	}
145	w.webdavhandler = webdavHandler
146	return w
147}
148
149// Gets the VFS in use for this request
150func (w *WebDAV) getVFS(ctx context.Context) (VFS *vfs.VFS, err error) {
151	if w._vfs != nil {
152		return w._vfs, nil
153	}
154	value := ctx.Value(httplib.ContextAuthKey)
155	if value == nil {
156		return nil, errors.New("no VFS found in context")
157	}
158	VFS, ok := value.(*vfs.VFS)
159	if !ok {
160		return nil, errors.Errorf("context value is not VFS: %#v", value)
161	}
162	return VFS, nil
163}
164
165// auth does proxy authorization
166func (w *WebDAV) auth(user, pass string) (value interface{}, err error) {
167	VFS, _, err := w.proxy.Call(user, pass, false)
168	if err != nil {
169		return nil, err
170	}
171	return VFS, err
172}
173
174func (w *WebDAV) handler(rw http.ResponseWriter, r *http.Request) {
175	urlPath, ok := w.Path(rw, r)
176	if !ok {
177		return
178	}
179	isDir := strings.HasSuffix(urlPath, "/")
180	remote := strings.Trim(urlPath, "/")
181	if !disableGETDir && (r.Method == "GET" || r.Method == "HEAD") && isDir {
182		w.serveDir(rw, r, remote)
183		return
184	}
185	w.webdavhandler.ServeHTTP(rw, r)
186}
187
188// serveDir serves a directory index at dirRemote
189// This is similar to serveDir in serve http.
190func (w *WebDAV) serveDir(rw http.ResponseWriter, r *http.Request, dirRemote string) {
191	VFS, err := w.getVFS(r.Context())
192	if err != nil {
193		http.Error(rw, "Root directory not found", http.StatusNotFound)
194		fs.Errorf(nil, "Failed to serve directory: %v", err)
195		return
196	}
197	// List the directory
198	node, err := VFS.Stat(dirRemote)
199	if err == vfs.ENOENT {
200		http.Error(rw, "Directory not found", http.StatusNotFound)
201		return
202	} else if err != nil {
203		serve.Error(dirRemote, rw, "Failed to list directory", err)
204		return
205	}
206	if !node.IsDir() {
207		http.Error(rw, "Not a directory", http.StatusNotFound)
208		return
209	}
210	dir := node.(*vfs.Dir)
211	dirEntries, err := dir.ReadDirAll()
212
213	if err != nil {
214		serve.Error(dirRemote, rw, "Failed to list directory", err)
215		return
216	}
217
218	// Make the entries for display
219	directory := serve.NewDirectory(dirRemote, w.HTMLTemplate)
220	for _, node := range dirEntries {
221		if vfsflags.Opt.NoModTime {
222			directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), time.Time{})
223		} else {
224			directory.AddHTMLEntry(node.Path(), node.IsDir(), node.Size(), node.ModTime().UTC())
225		}
226	}
227
228	sortParm := r.URL.Query().Get("sort")
229	orderParm := r.URL.Query().Get("order")
230	directory.ProcessQueryParams(sortParm, orderParm)
231
232	directory.Serve(rw, r)
233}
234
235// serve runs the http server in the background.
236//
237// Use s.Close() and s.Wait() to shutdown server
238func (w *WebDAV) serve() error {
239	err := w.Serve()
240	if err != nil {
241		return err
242	}
243	fs.Logf(w.f, "WebDav Server started on %s", w.URL())
244	return nil
245}
246
247// logRequest is called by the webdav module on every request
248func (w *WebDAV) logRequest(r *http.Request, err error) {
249	fs.Infof(r.URL.Path, "%s from %s", r.Method, r.RemoteAddr)
250}
251
252// Mkdir creates a directory
253func (w *WebDAV) Mkdir(ctx context.Context, name string, perm os.FileMode) (err error) {
254	// defer log.Trace(name, "perm=%v", perm)("err = %v", &err)
255	VFS, err := w.getVFS(ctx)
256	if err != nil {
257		return err
258	}
259	dir, leaf, err := VFS.StatParent(name)
260	if err != nil {
261		return err
262	}
263	_, err = dir.Mkdir(leaf)
264	return err
265}
266
267// OpenFile opens a file or a directory
268func (w *WebDAV) OpenFile(ctx context.Context, name string, flags int, perm os.FileMode) (file webdav.File, err error) {
269	// defer log.Trace(name, "flags=%v, perm=%v", flags, perm)("err = %v", &err)
270	VFS, err := w.getVFS(ctx)
271	if err != nil {
272		return nil, err
273	}
274	f, err := VFS.OpenFile(name, flags, perm)
275	if err != nil {
276		return nil, err
277	}
278	return Handle{f}, nil
279}
280
281// RemoveAll removes a file or a directory and its contents
282func (w *WebDAV) RemoveAll(ctx context.Context, name string) (err error) {
283	// defer log.Trace(name, "")("err = %v", &err)
284	VFS, err := w.getVFS(ctx)
285	if err != nil {
286		return err
287	}
288	node, err := VFS.Stat(name)
289	if err != nil {
290		return err
291	}
292	err = node.RemoveAll()
293	if err != nil {
294		return err
295	}
296	return nil
297}
298
299// Rename a file or a directory
300func (w *WebDAV) Rename(ctx context.Context, oldName, newName string) (err error) {
301	// defer log.Trace(oldName, "newName=%q", newName)("err = %v", &err)
302	VFS, err := w.getVFS(ctx)
303	if err != nil {
304		return err
305	}
306	return VFS.Rename(oldName, newName)
307}
308
309// Stat returns info about the file or directory
310func (w *WebDAV) Stat(ctx context.Context, name string) (fi os.FileInfo, err error) {
311	// defer log.Trace(name, "")("fi=%+v, err = %v", &fi, &err)
312	VFS, err := w.getVFS(ctx)
313	if err != nil {
314		return nil, err
315	}
316	fi, err = VFS.Stat(name)
317	if err != nil {
318		return nil, err
319	}
320	return FileInfo{fi}, nil
321}
322
323// Handle represents an open file
324type Handle struct {
325	vfs.Handle
326}
327
328// Readdir reads directory entries from the handle
329func (h Handle) Readdir(count int) (fis []os.FileInfo, err error) {
330	fis, err = h.Handle.Readdir(count)
331	if err != nil {
332		return nil, err
333	}
334	// Wrap each FileInfo
335	for i := range fis {
336		fis[i] = FileInfo{fis[i]}
337	}
338	return fis, nil
339}
340
341// Stat the handle
342func (h Handle) Stat() (fi os.FileInfo, err error) {
343	fi, err = h.Handle.Stat()
344	if err != nil {
345		return nil, err
346	}
347	return FileInfo{fi}, nil
348}
349
350// FileInfo represents info about a file satisfying os.FileInfo and
351// also some additional interfaces for webdav for ETag and ContentType
352type FileInfo struct {
353	os.FileInfo
354}
355
356// ETag returns an ETag for the FileInfo
357func (fi FileInfo) ETag(ctx context.Context) (etag string, err error) {
358	// defer log.Trace(fi, "")("etag=%q, err=%v", &etag, &err)
359	if hashType == hash.None {
360		return "", webdav.ErrNotImplemented
361	}
362	node, ok := (fi.FileInfo).(vfs.Node)
363	if !ok {
364		fs.Errorf(fi, "Expecting vfs.Node, got %T", fi.FileInfo)
365		return "", webdav.ErrNotImplemented
366	}
367	entry := node.DirEntry()
368	o, ok := entry.(fs.Object)
369	if !ok {
370		return "", webdav.ErrNotImplemented
371	}
372	hash, err := o.Hash(ctx, hashType)
373	if err != nil || hash == "" {
374		return "", webdav.ErrNotImplemented
375	}
376	return `"` + hash + `"`, nil
377}
378
379// ContentType returns a content type for the FileInfo
380func (fi FileInfo) ContentType(ctx context.Context) (contentType string, err error) {
381	// defer log.Trace(fi, "")("etag=%q, err=%v", &contentType, &err)
382	node, ok := (fi.FileInfo).(vfs.Node)
383	if !ok {
384		fs.Errorf(fi, "Expecting vfs.Node, got %T", fi.FileInfo)
385		return "application/octet-stream", nil
386	}
387	entry := node.DirEntry()
388	switch x := entry.(type) {
389	case fs.Object:
390		return fs.MimeType(ctx, x), nil
391	case fs.Directory:
392		return "inode/directory", nil
393	}
394	fs.Errorf(fi, "Expecting fs.Object or fs.Directory, got %T", entry)
395	return "application/octet-stream", nil
396}
397