1package webdavd
2
3import (
4	"context"
5	"net/http"
6	"os"
7	"path"
8	"strings"
9
10	"github.com/eikenb/pipeat"
11	"golang.org/x/net/webdav"
12
13	"github.com/drakkan/sftpgo/v2/common"
14	"github.com/drakkan/sftpgo/v2/dataprovider"
15	"github.com/drakkan/sftpgo/v2/logger"
16	"github.com/drakkan/sftpgo/v2/util"
17	"github.com/drakkan/sftpgo/v2/vfs"
18)
19
20// Connection details for a WebDav connection.
21type Connection struct {
22	*common.BaseConnection
23	request *http.Request
24}
25
26// GetClientVersion returns the connected client's version.
27func (c *Connection) GetClientVersion() string {
28	if c.request != nil {
29		return c.request.UserAgent()
30	}
31	return ""
32}
33
34// GetLocalAddress returns local connection address
35func (c *Connection) GetLocalAddress() string {
36	return util.GetHTTPLocalAddress(c.request)
37}
38
39// GetRemoteAddress returns the connected client's address
40func (c *Connection) GetRemoteAddress() string {
41	if c.request != nil {
42		return c.request.RemoteAddr
43	}
44	return ""
45}
46
47// Disconnect closes the active transfer
48func (c *Connection) Disconnect() error {
49	return c.SignalTransfersAbort()
50}
51
52// GetCommand returns the request method
53func (c *Connection) GetCommand() string {
54	if c.request != nil {
55		return strings.ToUpper(c.request.Method)
56	}
57	return ""
58}
59
60// Mkdir creates a directory using the connection filesystem
61func (c *Connection) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
62	c.UpdateLastActivity()
63
64	name = util.CleanPath(name)
65	return c.CreateDir(name)
66}
67
68// Rename renames a file or a directory
69func (c *Connection) Rename(ctx context.Context, oldName, newName string) error {
70	c.UpdateLastActivity()
71
72	oldName = util.CleanPath(oldName)
73	newName = util.CleanPath(newName)
74
75	return c.BaseConnection.Rename(oldName, newName)
76}
77
78// Stat returns a FileInfo describing the named file/directory, or an error,
79// if any happens
80func (c *Connection) Stat(ctx context.Context, name string) (os.FileInfo, error) {
81	c.UpdateLastActivity()
82
83	name = util.CleanPath(name)
84	if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(name)) {
85		return nil, c.GetPermissionDeniedError()
86	}
87
88	fi, err := c.DoStat(name, 0)
89	if err != nil {
90		c.Log(logger.LevelDebug, "error running stat on path %#v: %+v", name, err)
91		return nil, err
92	}
93	return fi, err
94}
95
96// RemoveAll removes path and any children it contains.
97// If the path does not exist, RemoveAll returns nil (no error).
98func (c *Connection) RemoveAll(ctx context.Context, name string) error {
99	c.UpdateLastActivity()
100
101	name = util.CleanPath(name)
102	fs, p, err := c.GetFsAndResolvedPath(name)
103	if err != nil {
104		return err
105	}
106
107	var fi os.FileInfo
108	if fi, err = fs.Lstat(p); err != nil {
109		c.Log(logger.LevelDebug, "failed to remove a file %#v: stat error: %+v", p, err)
110		return c.GetFsError(fs, err)
111	}
112
113	if fi.IsDir() && fi.Mode()&os.ModeSymlink == 0 {
114		return c.removeDirTree(fs, p, name)
115	}
116	return c.RemoveFile(fs, p, name, fi)
117}
118
119// OpenFile opens the named file with specified flag.
120// This method is used for uploads and downloads but also for Stat and Readdir
121func (c *Connection) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
122	c.UpdateLastActivity()
123
124	name = util.CleanPath(name)
125	fs, p, err := c.GetFsAndResolvedPath(name)
126	if err != nil {
127		return nil, err
128	}
129
130	if flag == os.O_RDONLY || c.request.Method == "PROPPATCH" {
131		// Download, Stat, Readdir or simply open/close
132		return c.getFile(fs, p, name)
133	}
134	return c.putFile(fs, p, name)
135}
136
137func (c *Connection) getFile(fs vfs.Fs, fsPath, virtualPath string) (webdav.File, error) {
138	var err error
139	var file vfs.File
140	var r *pipeat.PipeReaderAt
141	var cancelFn func()
142
143	// for cloud fs we open the file when we receive the first read to avoid to download the first part of
144	// the file if it was opened only to do a stat or a readdir and so it is not a real download
145	if vfs.IsLocalOrUnbufferedSFTPFs(fs) {
146		file, r, cancelFn, err = fs.Open(fsPath, 0)
147		if err != nil {
148			c.Log(logger.LevelWarn, "could not open file %#v for reading: %+v", fsPath, err)
149			return nil, c.GetFsError(fs, err)
150		}
151	}
152
153	baseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, fsPath, fsPath, virtualPath, common.TransferDownload,
154		0, 0, 0, false, fs)
155
156	return newWebDavFile(baseTransfer, nil, r), nil
157}
158
159func (c *Connection) putFile(fs vfs.Fs, fsPath, virtualPath string) (webdav.File, error) {
160	if !c.User.IsFileAllowed(virtualPath) {
161		c.Log(logger.LevelWarn, "writing file %#v is not allowed", virtualPath)
162		return nil, c.GetPermissionDeniedError()
163	}
164
165	filePath := fsPath
166	if common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {
167		filePath = fs.GetAtomicUploadPath(fsPath)
168	}
169
170	stat, statErr := fs.Lstat(fsPath)
171	if (statErr == nil && stat.Mode()&os.ModeSymlink != 0) || fs.IsNotExist(statErr) {
172		if !c.User.HasPerm(dataprovider.PermUpload, path.Dir(virtualPath)) {
173			return nil, c.GetPermissionDeniedError()
174		}
175		return c.handleUploadToNewFile(fs, fsPath, filePath, virtualPath)
176	}
177
178	if statErr != nil {
179		c.Log(logger.LevelError, "error performing file stat %#v: %+v", fsPath, statErr)
180		return nil, c.GetFsError(fs, statErr)
181	}
182
183	// This happen if we upload a file that has the same name of an existing directory
184	if stat.IsDir() {
185		c.Log(logger.LevelWarn, "attempted to open a directory for writing to: %#v", fsPath)
186		return nil, c.GetOpUnsupportedError()
187	}
188
189	if !c.User.HasPerm(dataprovider.PermOverwrite, path.Dir(virtualPath)) {
190		return nil, c.GetPermissionDeniedError()
191	}
192
193	return c.handleUploadToExistingFile(fs, fsPath, filePath, stat.Size(), virtualPath)
194}
195
196func (c *Connection) handleUploadToNewFile(fs vfs.Fs, resolvedPath, filePath, requestPath string) (webdav.File, error) {
197	quotaResult := c.HasSpace(true, false, requestPath)
198	if !quotaResult.HasSpace {
199		c.Log(logger.LevelInfo, "denying file write due to quota limits")
200		return nil, common.ErrQuotaExceeded
201	}
202	if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), c.GetRemoteIP(), 0, 0); err != nil {
203		c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
204		return nil, c.GetPermissionDeniedError()
205	}
206	file, w, cancelFn, err := fs.Create(filePath, 0)
207	if err != nil {
208		c.Log(logger.LevelWarn, "error creating file %#v: %+v", resolvedPath, err)
209		return nil, c.GetFsError(fs, err)
210	}
211
212	vfs.SetPathPermissions(fs, filePath, c.User.GetUID(), c.User.GetGID())
213
214	// we can get an error only for resume
215	maxWriteSize, _ := c.GetMaxWriteSize(quotaResult, false, 0, fs.IsUploadResumeSupported())
216
217	baseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, resolvedPath, filePath, requestPath,
218		common.TransferUpload, 0, 0, maxWriteSize, true, fs)
219
220	return newWebDavFile(baseTransfer, w, nil), nil
221}
222
223func (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePath string, fileSize int64,
224	requestPath string) (webdav.File, error) {
225	var err error
226	quotaResult := c.HasSpace(false, false, requestPath)
227	if !quotaResult.HasSpace {
228		c.Log(logger.LevelInfo, "denying file write due to quota limits")
229		return nil, common.ErrQuotaExceeded
230	}
231	if err := common.ExecutePreAction(&c.User, common.OperationPreUpload, resolvedPath, requestPath, c.GetProtocol(), c.GetRemoteIP(),
232		fileSize, os.O_TRUNC); err != nil {
233		c.Log(logger.LevelDebug, "upload for file %#v denied by pre action: %v", requestPath, err)
234		return nil, c.GetPermissionDeniedError()
235	}
236
237	// if there is a size limit remaining size cannot be 0 here, since quotaResult.HasSpace
238	// will return false in this case and we deny the upload before
239	maxWriteSize, _ := c.GetMaxWriteSize(quotaResult, false, fileSize, fs.IsUploadResumeSupported())
240
241	if common.Config.IsAtomicUploadEnabled() && fs.IsAtomicUploadSupported() {
242		err = fs.Rename(resolvedPath, filePath)
243		if err != nil {
244			c.Log(logger.LevelWarn, "error renaming existing file for atomic upload, source: %#v, dest: %#v, err: %+v",
245				resolvedPath, filePath, err)
246			return nil, c.GetFsError(fs, err)
247		}
248	}
249
250	file, w, cancelFn, err := fs.Create(filePath, 0)
251	if err != nil {
252		c.Log(logger.LevelWarn, "error creating file %#v: %+v", resolvedPath, err)
253		return nil, c.GetFsError(fs, err)
254	}
255	initialSize := int64(0)
256	if vfs.IsLocalOrSFTPFs(fs) {
257		vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
258		if err == nil {
259			dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck
260			if vfolder.IsIncludedInUserQuota() {
261				dataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck
262			}
263		} else {
264			dataprovider.UpdateUserQuota(&c.User, 0, -fileSize, false) //nolint:errcheck
265		}
266	} else {
267		initialSize = fileSize
268	}
269
270	vfs.SetPathPermissions(fs, filePath, c.User.GetUID(), c.User.GetGID())
271
272	baseTransfer := common.NewBaseTransfer(file, c.BaseConnection, cancelFn, resolvedPath, filePath, requestPath,
273		common.TransferUpload, 0, initialSize, maxWriteSize, false, fs)
274
275	return newWebDavFile(baseTransfer, w, nil), nil
276}
277
278type objectMapping struct {
279	fsPath      string
280	virtualPath string
281	info        os.FileInfo
282}
283
284func (c *Connection) removeDirTree(fs vfs.Fs, fsPath, virtualPath string) error {
285	var dirsToRemove []objectMapping
286	var filesToRemove []objectMapping
287
288	err := fs.Walk(fsPath, func(walkedPath string, info os.FileInfo, err error) error {
289		if err != nil {
290			return err
291		}
292
293		obj := objectMapping{
294			fsPath:      walkedPath,
295			virtualPath: fs.GetRelativePath(walkedPath),
296			info:        info,
297		}
298		if info.IsDir() {
299			err = c.IsRemoveDirAllowed(fs, obj.fsPath, obj.virtualPath)
300			isDuplicated := false
301			for _, d := range dirsToRemove {
302				if d.fsPath == obj.fsPath {
303					isDuplicated = true
304					break
305				}
306			}
307			if !isDuplicated {
308				dirsToRemove = append(dirsToRemove, obj)
309			}
310		} else {
311			err = c.IsRemoveFileAllowed(obj.virtualPath)
312			filesToRemove = append(filesToRemove, obj)
313		}
314		if err != nil {
315			c.Log(logger.LevelDebug, "unable to remove dir tree, object %#v->%#v cannot be removed: %v",
316				virtualPath, fsPath, err)
317			return err
318		}
319
320		return nil
321	})
322	if err != nil {
323		c.Log(logger.LevelWarn, "failed to remove dir tree %#v->%#v: error: %+v", virtualPath, fsPath, err)
324		return err
325	}
326
327	for _, fileObj := range filesToRemove {
328		err = c.RemoveFile(fs, fileObj.fsPath, fileObj.virtualPath, fileObj.info)
329		if err != nil {
330			c.Log(logger.LevelDebug, "unable to remove dir tree, error removing file %#v->%#v: %v",
331				fileObj.virtualPath, fileObj.fsPath, err)
332			return err
333		}
334	}
335
336	for _, dirObj := range c.orderDirsToRemove(fs, dirsToRemove) {
337		err = c.RemoveDir(dirObj.virtualPath)
338		if err != nil {
339			c.Log(logger.LevelDebug, "unable to remove dir tree, error removing directory %#v->%#v: %v",
340				dirObj.virtualPath, dirObj.fsPath, err)
341			return err
342		}
343	}
344
345	return err
346}
347
348// order directories so that the empty ones will be at slice start
349func (c *Connection) orderDirsToRemove(fs vfs.Fs, dirsToRemove []objectMapping) []objectMapping {
350	orderedDirs := make([]objectMapping, 0, len(dirsToRemove))
351	removedDirs := make([]string, 0, len(dirsToRemove))
352
353	pathSeparator := "/"
354	if vfs.IsLocalOsFs(fs) {
355		pathSeparator = string(os.PathSeparator)
356	}
357
358	for len(orderedDirs) < len(dirsToRemove) {
359		for idx, d := range dirsToRemove {
360			if util.IsStringInSlice(d.fsPath, removedDirs) {
361				continue
362			}
363			isEmpty := true
364			for idx1, d1 := range dirsToRemove {
365				if idx == idx1 {
366					continue
367				}
368				if util.IsStringInSlice(d1.fsPath, removedDirs) {
369					continue
370				}
371				if strings.HasPrefix(d1.fsPath, d.fsPath+pathSeparator) {
372					isEmpty = false
373					break
374				}
375			}
376			if isEmpty {
377				orderedDirs = append(orderedDirs, d)
378				removedDirs = append(removedDirs, d.fsPath)
379			}
380		}
381	}
382
383	return orderedDirs
384}
385