feature (webdav): shared links as webdav server

This commit is contained in:
Mickael KERJEAN
2019-01-21 18:10:14 +11:00
parent ec6eb0e8fa
commit c3d34e6f87
9 changed files with 334 additions and 155 deletions

View File

@ -85,11 +85,11 @@
/* Font stuff */ /* Font stuff */
.CodeMirror { .CodeMirror {
font-size: 15px; font-size: 16px;
font-family: 'Source Code Pro', monospace; font-family: 'Source Code Pro', monospace;
} }
.cm-s-default .cm-header { font-size: 17px; } .cm-s-default .cm-header { font-size: 18px; }
.cm-s-default .cm-header.cm-level1{ font-size: 18px;} .cm-s-default .cm-header.cm-level1{ font-size: 19px;}
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
.CodeMirror { font-size: 14px; } .CodeMirror { font-size: 14px; }

View File

@ -1,7 +1,7 @@
package common package common
const ( const (
APP_VERSION = "v0.3" APP_VERSION = "v0.4rc"
CONFIG_PATH = "data/config/" CONFIG_PATH = "data/config/"
PLUGIN_PATH = "data/plugin/" PLUGIN_PATH = "data/plugin/"
LOG_PATH = "data/log/" LOG_PATH = "data/log/"

View File

@ -48,17 +48,32 @@ func Hash(str string, n int) string {
hasher := sha256.New() hasher := sha256.New()
hasher.Write([]byte(str)) hasher.Write([]byte(str))
d := hasher.Sum(nil) d := hasher.Sum(nil)
size := len(Letters)
h := "" h := ""
for i:=0; i<len(d); i++ { for i:=0; i<len(d); i++ {
if n > 0 && i >= n { if n > 0 && len(h) >= n {
break break
} }
h += string(Letters[int(d[i]) % size]) h += ReversedBaseChange(Letters, int(d[i]))
}
if len(h) > n {
return h[0:len(h) - 1]
} }
return h return h
} }
func ReversedBaseChange(alphabet []rune, i int) string {
str := ""
for {
str += string(alphabet[i % len(alphabet)])
i = i / len(alphabet)
if i == 0 {
break
}
}
return str
}
func RandomString(n int) string { func RandomString(n int) string {
b := make([]rune, n) b := make([]rune, n)
for i := range b { for i := range b {

View File

@ -87,10 +87,10 @@ func(this SafeMapStringString) Set(key string, value string) {
func(this SafeMapStringString) Gets(keys ...string) []string{ func(this SafeMapStringString) Gets(keys ...string) []string{
this.RLock() this.RLock()
defer this.RUnlock()
res := make([]string, len(keys)) res := make([]string, len(keys))
for i, key := range keys { for i, key := range keys {
res[i] = this.internal[key] res[i] = this.internal[key]
} }
this.RUnlock()
return res return res
} }

View File

@ -5,6 +5,8 @@ import (
"github.com/mickael-kerjean/filestash/server/model" "github.com/mickael-kerjean/filestash/server/model"
"github.com/mickael-kerjean/net/webdav" "github.com/mickael-kerjean/net/webdav"
"net/http" "net/http"
"path/filepath"
"strings"
) )
func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) { func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) {
@ -13,11 +15,115 @@ func WebdavHandler(ctx App, res http.ResponseWriter, req *http.Request) {
return return
} }
// https://github.com/golang/net/blob/master/webdav/webdav.go#L49-L68
canRead := model.CanRead(&ctx)
canWrite := model.CanRead(&ctx)
canUpload := model.CanUpload(&ctx)
switch req.Method {
case "OPTIONS", "GET", "HEAD", "POST", "PROPFIND":
if canRead == false {
SendErrorResult(res, ErrPermissionDenied)
return return
}
case "MKCOL", "DELETE", "COPY", "MOVE", "PROPPATCH":
if canWrite == false {
SendErrorResult(res, ErrPermissionDenied)
return
}
case "PUT", "LOCK", "UNLOCK":
if canWrite == false && canUpload == false {
SendErrorResult(res, ErrPermissionDenied)
return
}
default:
SendErrorResult(res, ErrNotImplemented)
return
}
h := &webdav.Handler{ h := &webdav.Handler{
Prefix: "/s/" + ctx.Share.Id, Prefix: "/s/" + ctx.Share.Id,
FileSystem: model.NewWebdavFs(ctx.Backend, ctx.Share.Path), FileSystem: model.NewWebdavFs(ctx.Backend, ctx.Share.Backend, ctx.Share.Path),
LockSystem: webdav.NewMemLS(), LockSystem: webdav.NewMemLS(),
} }
h.ServeHTTP(res, req) h.ServeHTTP(res, req)
} }
/*
* OSX ask for a lot of crap while mounting as a network drive. To avoid wasting resources with such
* an imbecile and considering we can't even see the source code they are running, the best approach we
* could go on is: "crap in, crap out" where useless request coming in are identified and answer appropriatly
*/
func WebdavBlacklist (fn func(App, http.ResponseWriter, *http.Request)) func(ctx App, res http.ResponseWriter, req *http.Request) {
return func(ctx App, res http.ResponseWriter, req *http.Request) {
base := filepath.Base(req.URL.String())
if req.Method == "PUT" || req.Method == "MKCOL" {
if strings.HasPrefix(base, "._") {
res.WriteHeader(http.StatusMethodNotAllowed)
res.Write([]byte(""))
return
} else if base == ".DS_Store" {
res.WriteHeader(http.StatusMethodNotAllowed)
res.Write([]byte(""))
return
} else if base == ".localized" {
res.WriteHeader(http.StatusMethodNotAllowed)
res.Write([]byte(""))
return
}
} else if req.Method == "PROPFIND" {
if strings.HasPrefix(base, "._") {
res.WriteHeader(http.StatusForbidden)
return
} else if base == ".DS_Store" {
res.WriteHeader(http.StatusForbidden)
res.Write([]byte(""))
return
} else if base == ".localized" {
res.WriteHeader(http.StatusForbidden)
return
} else if base == ".ql_disablethumbnails" {
res.WriteHeader(http.StatusForbidden)
res.Write([]byte(""))
return
} else if base == ".ql_disablecache" {
res.WriteHeader(http.StatusForbidden)
return
} else if base == ".hidden" {
res.WriteHeader(http.StatusForbidden)
return
} else if base == ".Spotlight-V100" {
res.WriteHeader(http.StatusForbidden)
return
} else if base == ".metadata_never_index" {
res.WriteHeader(http.StatusForbidden)
return
} else if base == "Contents" {
res.WriteHeader(http.StatusForbidden)
return
} else if base == ".metadata_never_index_unless_rootfs" {
res.WriteHeader(http.StatusForbidden)
return
}
} else if req.Method == "GET" {
if base == ".DS_Store" {
res.WriteHeader(http.StatusForbidden)
res.Write([]byte(""))
return
}
} else if req.Method == "DELETE" {
if base == ".DS_Store" {
res.WriteHeader(http.StatusForbidden)
res.Write([]byte(""))
return
}
} else if req.Method == "LOCK" || req.Method == "UNLOCK" {
if base == ".DS_Store" {
res.WriteHeader(http.StatusMethodNotAllowed)
res.Write([]byte(""))
return
}
}
fn(ctx, res, req)
}
}

View File

@ -93,7 +93,7 @@ func Init(a *App) {
// Webdav server / Shared Link // Webdav server / Shared Link
middlewares = []Middleware{ IndexHeaders, SecureHeaders } middlewares = []Middleware{ IndexHeaders, SecureHeaders }
r.HandleFunc("/s/{share}", NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, *a)).Methods("GET") r.HandleFunc("/s/{share}", NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, *a)).Methods("GET")
middlewares = []Middleware{ SessionStart } middlewares = []Middleware{ WebdavBlacklist, SessionStart }
r.PathPrefix("/s/{share}").Handler(NewMiddlewareChain(WebdavHandler, middlewares, *a)) r.PathPrefix("/s/{share}").Handler(NewMiddlewareChain(WebdavHandler, middlewares, *a))
// Application Resources // Application Resources

View File

@ -246,6 +246,9 @@ func (b Sftp) Close() error {
func (b Sftp) err(e error) error { func (b Sftp) err(e error) error {
f, ok := e.(*sftp.StatusError) f, ok := e.(*sftp.StatusError)
if ok == false { if ok == false {
if e == os.ErrNotExist {
return ErrNotFound
}
return e return e
} }
switch f.Code { switch f.Code {
@ -258,7 +261,7 @@ func (b Sftp) err(e error) error {
case 3: case 3:
return NewError("Permission denied", 403) return NewError("Permission denied", 403)
case 4: case 4:
return NewError("Failure", 400) return NewError("Failure", 409)
case 5: case 5:
return NewError("Not Compatible", 400) return NewError("Not Compatible", 400)
case 6: case 6:

View File

@ -89,7 +89,9 @@ func (w WebDav) Ls(path string) ([]os.FileInfo, error) {
<d:getlastmodified/> <d:getlastmodified/>
</d:prop> </d:prop>
</d:propfind>` </d:propfind>`
res, err := w.request("PROPFIND", w.params.url+encodeURL(path), strings.NewReader(query), nil) res, err := w.request("PROPFIND", w.params.url+encodeURL(path), strings.NewReader(query), func(req *http.Request) {
req.Header.Add("Depth", "1")
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -111,6 +113,7 @@ func (w WebDav) Ls(path string) ([]os.FileInfo, error) {
if tag.Href == ShortURLDav || tag.Href == LongURLDav { if tag.Href == ShortURLDav || tag.Href == LongURLDav {
continue continue
} }
for i, prop := range tag.Props { for i, prop := range tag.Props {
if i > 0 { if i > 0 {
break break

View File

@ -1,13 +1,15 @@
package model package model
import ( import (
"os"
"context" "context"
"fmt"
. "github.com/mickael-kerjean/filestash/server/common" . "github.com/mickael-kerjean/filestash/server/common"
"github.com/mickael-kerjean/net/webdav" "github.com/mickael-kerjean/net/webdav"
"io"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"io" "time"
) )
const DAVCachePath = "data/cache/webdav/" const DAVCachePath = "data/cache/webdav/"
@ -19,195 +21,245 @@ func init() {
os.MkdirAll(cachePath, os.ModePerm) os.MkdirAll(cachePath, os.ModePerm)
} }
/*
* Implement a webdav.FileSystem: https://godoc.org/golang.org/x/net/webdav#FileSystem
*/
type WebdavFs struct { type WebdavFs struct {
backend IBackend backend IBackend
path string path string
id string
chroot string
} }
func NewWebdavFs(b IBackend, path string) WebdavFs { func NewWebdavFs(b IBackend, primaryKey string, chroot string) WebdavFs {
return WebdavFs{ return WebdavFs{
backend: b, backend: b,
path: path, id: primaryKey,
chroot: chroot,
} }
} }
func (fs WebdavFs) Mkdir(ctx context.Context, name string, perm os.FileMode) error { func (this WebdavFs) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
Log.Info("MKDIR ('%s')", name) if name = this.fullpath(name); name == "" {
if name = fs.resolve(name); name == "" { return os.ErrNotExist
return os.ErrInvalid
} }
return fs.backend.Mkdir(name) return this.backend.Mkdir(name)
} }
func (fs WebdavFs) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { func (this WebdavFs) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
Log.Info("OPEN_FILE ('%s')", name) if name = this.fullpath(name); name == "" {
return NewWebdavNode(name, fs), nil return nil, os.ErrNotExist
}
return &WebdavFile{
path: name,
backend: this.backend,
cache: fmt.Sprintf("%stmp_%s", cachePath, Hash(this.id + name, 20)),
}, nil
} }
func (fs WebdavFs) RemoveAll(ctx context.Context, name string) error { func (this WebdavFs) RemoveAll(ctx context.Context, name string) error {
Log.Info("RM ('%s')", name) if name = this.fullpath(name); name == "" {
if name = fs.resolve(name); name == "" { return os.ErrNotExist
return os.ErrInvalid
} }
return fs.backend.Rm(name) return this.backend.Rm(name)
} }
func (fs WebdavFs) Rename(ctx context.Context, oldName, newName string) error { func (this WebdavFs) Rename(ctx context.Context, oldName, newName string) error {
Log.Info("MV ('%s' => '%s')", oldName, newName) if oldName = this.fullpath(oldName); oldName == "" {
if oldName = fs.resolve(oldName); oldName == "" { return os.ErrNotExist
return os.ErrInvalid } else if newName = this.fullpath(newName); newName == "" {
return os.ErrNotExist
} }
if newName = fs.resolve(newName); newName == "" { return this.backend.Mv(oldName, newName)
return os.ErrInvalid
}
return fs.backend.Mv(oldName, newName)
} }
func (fs WebdavFs) Stat(ctx context.Context, name string) (os.FileInfo, error) { func (this WebdavFs) Stat(ctx context.Context, name string) (os.FileInfo, error) {
Log.Info("STAT ('%s')", name) if name = this.fullpath(name); name == "" {
if name = fs.resolve(name); name == "" { return nil, os.ErrNotExist
return nil, os.ErrInvalid
} }
return WebdavFile{
if obj, ok := fs.backend.(interface{ Stat(path string) (os.FileInfo, error) }); ok { path: name,
return obj.Stat(name) backend: this.backend,
} cache: fmt.Sprintf("%stmp_%s", cachePath, Hash(this.id + name, 20)),
return nil, os.ErrInvalid }.Stat()
} }
func (fs WebdavFs) resolve(path string) string { func (this WebdavFs) fullpath(path string) string {
p := filepath.Join(fs.path, path) p := filepath.Join(this.chroot, path)
if strings.HasSuffix(path, "/") == true && strings.HasSuffix(p, "/") == false { if strings.HasSuffix(path, "/") == true && strings.HasSuffix(p, "/") == false {
p += "/" p += "/"
} }
if strings.HasPrefix(p, fs.path) == true { if strings.HasPrefix(p, this.chroot) == false {
return p
}
return "" return ""
}
return p
} }
type WebdavNode struct { /*
fs WebdavFs * Implement a webdav.File and os.Stat : https://godoc.org/golang.org/x/net/webdav#File
*/
type WebdavFile struct {
path string path string
fileread *os.File backend IBackend
filewrite *os.File cache string
fread *os.File
fwrite *os.File
} }
func NewWebdavNode(name string, fs WebdavFs) *WebdavNode { func (this *WebdavFile) Read(p []byte) (n int, err error) {
return &WebdavNode{ if strings.HasPrefix(filepath.Base(this.path), ".") {
fs: fs, return 0, os.ErrNotExist
path: name,
}
}
func (w *WebdavNode) Readdir(count int) ([]os.FileInfo, error) {
Log.Info(" => READ_DIR ('%s')", w.path)
var path string
if path = w.fs.resolve(w.path); path == "" {
return nil, os.ErrInvalid
}
return w.fs.backend.Ls(path)
}
func (w *WebdavNode) Stat() (os.FileInfo, error) {
Log.Info(" => STAT ('%s')", w.path)
// if w.filewrite != nil {
// var path stringc
// var err error
// if path = w.fs.resolve(w.path); path == "" {
// return nil, os.ErrInvalid
// }
// name := w.filewrite.Name()
// w.filewrite.Close()
// if w.filewrite, err = os.OpenFile(name, os.O_RDONLY, os.ModePerm); err != nil {
// return nil, os.ErrInvalid
// }
// if err = w.fs.backend.Save(path, w.filewrite); err != nil {
// return nil, err
// }
// }
return w.fs.Stat(context.Background(), w.path)
}
func (w *WebdavNode) Close() error {
Log.Info(" => CLOSE ('%s')", w.path)
if w.fileread != nil {
if err := w.cleanup(w.fileread); err != nil {
return err
}
w.fileread = nil
}
if w.filewrite != nil {
defer w.cleanup(w.filewrite)
name := w.filewrite.Name()
w.filewrite.Close()
reader, err := os.OpenFile(name, os.O_RDONLY, os.ModePerm);
if err != nil {
return os.ErrInvalid
}
path := w.fs.resolve(w.path)
if path == "" {
return os.ErrInvalid
}
if err := w.fs.backend.Save(path, reader); err != nil {
return err
}
reader.Close()
}
return nil
}
func (w *WebdavNode) Read(p []byte) (int, error) {
Log.Info(" => READ ('%s')", w.path)
if w.fileread != nil {
return w.fileread.Read(p)
} }
if this.fread == nil {
if this.fread = this.pull_remote_file(); this.fread == nil {
return -1, os.ErrInvalid return -1, os.ErrInvalid
}
}
return this.fread.Read(p)
} }
func (w *WebdavNode) Seek(offset int64, whence int) (int64, error) { func (this *WebdavFile) Close() error {
Log.Info(" => SEEK ('%s')", w.path) if this.fread != nil {
var path string name := this.fread.Name()
var err error if this.fread.Close() == nil {
if path = w.fs.resolve(w.path); path == "" { this.fread = nil
return 0, os.ErrInvalid
} }
if this.fwrite != nil {
if w.fileread == nil { // while writing something, we flush any cache to avoid being out of sync
var reader io.Reader os.Remove(name)
if w.fileread, err = os.OpenFile(cachePath + "tmp_" + QuickString(10), os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.ModePerm); err != nil { return nil
return 0, os.ErrInvalid
}
if reader, err = w.fs.backend.Cat(path); err != nil {
return 0, os.ErrInvalid
}
io.Copy(w.fileread, reader)
name := w.fileread.Name()
w.fileread.Close()
w.fileread, err = os.OpenFile(name, os.O_RDONLY, os.ModePerm)
}
return w.fileread.Seek(offset, whence)
}
func (w *WebdavNode) Write(p []byte) (int, error) {
Log.Info(" => WRITE ('%s')", w.path)
var err error
if w.filewrite == nil {
if w.filewrite, err = os.OpenFile(cachePath + "tmp_" + QuickString(10), os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.ModePerm); err != nil {
return 0, os.ErrInvalid
} }
} }
return w.filewrite.Write(p) if this.fwrite != nil {
} // save the cache that's been written to disk in the remote storage
name := this.fwrite.Name()
func (w *WebdavNode) cleanup(file *os.File) error { if this.fwrite.Close() == nil {
name := file.Name() this.fwrite = nil
file.Close(); }
os.Remove(name); if f, err := os.OpenFile(name+"_writer", os.O_RDONLY, os.ModePerm); err == nil {
this.backend.Save(this.path, f)
f.Close()
}
os.Remove(name)
}
return nil return nil
} }
func (this *WebdavFile) Seek(offset int64, whence int) (int64, error) {
if this.fread == nil {
this.fread = this.pull_remote_file();
if this.fread == nil {
return offset, ErrNotFound
}
}
a, err := this.fread.Seek(offset, whence)
if err != nil {
return a, ErrNotFound
}
return a, nil
}
func (this WebdavFile) Readdir(count int) ([]os.FileInfo, error) {
if strings.HasPrefix(filepath.Base(this.path), ".") {
return nil, os.ErrNotExist
}
return this.backend.Ls(this.path)
}
func (this WebdavFile) Stat() (os.FileInfo, error) {
return &this, nil
}
func (this *WebdavFile) Write(p []byte) (int, error) {
if strings.HasPrefix(filepath.Base(this.path), ".") {
return 0, os.ErrNotExist
}
if this.fwrite == nil {
f, err := os.OpenFile(this.cache+"_writer", os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.ModePerm);
if err != nil {
return 0, os.ErrInvalid
}
this.fwrite = f
}
return this.fwrite.Write(p)
}
func (this WebdavFile) pull_remote_file() *os.File {
filename := this.cache+"_reader"
defer removeIn2Minutes(filename)
if f, err := os.OpenFile(filename, os.O_RDONLY, os.ModePerm); err == nil {
return f
}
if f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, os.ModePerm); err == nil {
if reader, err := this.backend.Cat(this.path); err == nil {
io.Copy(f, reader)
f.Close()
if obj, ok := reader.(interface{ Close() error }); ok {
obj.Close()
}
if f, err = os.OpenFile(filename, os.O_RDONLY, os.ModePerm); err == nil {
return f
}
return nil
}
f.Close()
}
return nil
}
func (this WebdavFile) Name() string {
return filepath.Base(this.path)
}
func (this *WebdavFile) Size() int64 {
if this.fread == nil {
if this.fread = this.pull_remote_file(); this.fread == nil {
return 0
}
}
if info, err := this.fread.Stat(); err == nil {
return info.Size()
}
return 0
}
func (this WebdavFile) Mode() os.FileMode {
return 0
}
func (this WebdavFile) ModTime() time.Time {
return time.Now()
}
func (this WebdavFile) IsDir() bool {
if strings.HasSuffix(this.path, "/") {
return true
}
return false
}
func (this WebdavFile) Sys() interface{} {
return nil
}
func (this WebdavFile) ETag(ctx context.Context) (string, error) {
// Building an etag can be an expensive call if the data isn't available locally.
// => 2 etags strategies:
// - use a legit etag value when the data is already in our cache
// - use a dummy value that's changing all the time when we don't have much info
etag := Hash(fmt.Sprintf("%d%s", this.ModTime().UnixNano(), this.path), 20)
if this.fread != nil {
if s, err := this.fread.Stat(); err == nil {
etag = Hash(fmt.Sprintf(`"%x%x"`, this.path, s.Size()), 20)
}
}
return etag, nil
}
func removeIn2Minutes(name string) {
go func(){
time.Sleep(120 * time.Second)
os.Remove(name)
}()
}