├── README.md ├── client.go ├── constants.go ├── docs.go ├── example ├── client.go └── server.go ├── filesystem.go ├── lock.go ├── node.go ├── node_test.go ├── server.go └── util.go /README.md: -------------------------------------------------------------------------------- 1 | # webdav 2 | 3 | __TODO (litmus tests):__ 4 | * basic..... pass 5 | * copymove.. pass 6 | * props..... FAILED 7 | * init.................. pass 8 | * begin................. pass 9 | * propfind_invalid...... pass 10 | * propfind_invalid2..... pass 11 | * propfind_d0........... pass 12 | * propinit.............. pass 13 | * propset............... FAIL 14 | * propget............... SKIPPED 15 | * propextended.......... pass 16 | * propmove.............. SKIPPED 17 | * propget............... SKIPPED 18 | * propdeletes........... SKIPPED 19 | * propget............... SKIPPED 20 | * propreplace........... SKIPPED 21 | * propget............... SKIPPED 22 | * propnullns............ SKIPPED 23 | * propget............... SKIPPED 24 | * prophighunicode....... SKIPPED 25 | * propget............... SKIPPED 26 | * propremoveset......... SKIPPED 27 | * propget............... SKIPPED 28 | * propsetremove......... SKIPPED 29 | * propget............... SKIPPED 30 | * propvalnspace......... SKIPPED 31 | * propwformed........... pass 32 | * propinit.............. pass 33 | * propmanyns............ FAIL 34 | * propget............... FAIL 35 | * propcleanup........... pass 36 | * finish................ pass 37 | * locks..... SKIPPED 38 | * http...... SKIPPED 39 | 40 | __future:__ 41 | * server 42 | * client 43 | * replace litmus test with plain go tests 44 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package webdav 2 | 3 | import () 4 | 5 | type FileSystemCloser interface { 6 | FileSystem 7 | Close() error 8 | } 9 | 10 | func Dial(url string) (FileSystemCloser, error) { 11 | // TODO 12 | return nil, ErrNotImplemented 13 | } 14 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package webdav 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | ) 7 | 8 | // status codes 9 | const ( 10 | StatusOK = http.StatusOK 11 | StatusCreated = http.StatusCreated 12 | StatusAccepted = http.StatusAccepted 13 | StatusNoContent = http.StatusNoContent 14 | StatusMovedPermanently = http.StatusMovedPermanently 15 | StatusMovedTemporarily = 302 // TODO: duplicate of http.StatusFound ? 16 | StatusNotModified = http.StatusNotModified 17 | StatusBadRequest = http.StatusBadRequest 18 | StatusUnauthorized = http.StatusUnauthorized 19 | StatusForbidden = http.StatusForbidden 20 | StatusNotFound = http.StatusNotFound 21 | StatusInternalServerError = http.StatusInternalServerError 22 | StatusNotImplemented = http.StatusNotImplemented 23 | StatusBadGateway = http.StatusBadGateway 24 | StatusServiceUnavailable = http.StatusServiceUnavailable 25 | StatusContinue = http.StatusContinue 26 | StatusMethodNotAllowed = http.StatusMethodNotAllowed 27 | StatusConflict = http.StatusConflict 28 | StatusPreconditionFailed = http.StatusPreconditionFailed 29 | StatusRequestTooLong = http.StatusRequestEntityTooLarge 30 | StatusUnsupportedMediaType = http.StatusUnsupportedMediaType 31 | ) 32 | 33 | // extended status codes, http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11 34 | const ( 35 | StatusMulti = 207 36 | StatusUnprocessableEntity = 422 37 | StatusLocked = 423 38 | StatusFailedDependency = 424 39 | StatusInsufficientStorage = 507 40 | ) 41 | 42 | var statusText = map[int]string{ 43 | StatusMovedTemporarily: "Moved Temporarily", 44 | StatusMulti: "Multi-Status", 45 | StatusUnprocessableEntity: "Unprocessable Entity", 46 | StatusLocked: "Locked", 47 | StatusFailedDependency: "Failed Dependency", 48 | StatusInsufficientStorage: "Insufficient Storage", 49 | } 50 | 51 | // StatusText returns a text for the HTTP status code. It returns the empty string if the code is unknown. 52 | func StatusText(code int) string { 53 | if t, ok := statusText[code]; ok { 54 | return t 55 | } 56 | 57 | return http.StatusText(code) 58 | } 59 | 60 | // internal error variables 61 | var ( 62 | ErrInvalidCharPath = errors.New("invalid character in file path") 63 | ErrNotImplemented = errors.New("feature not yet implemented") 64 | ErrMalformedXml = errors.New("xml is not well-formed") 65 | ) 66 | -------------------------------------------------------------------------------- /docs.go: -------------------------------------------------------------------------------- 1 | /* 2 | package webdav provides WEBDAV client and server implementations 3 | 4 | webdav java 5 | http://svn.apache.org/repos/asf/tomcat/tc7.0.x/trunk/java/org/apache/catalina/servlets/WebdavServlet.java 6 | http://svn.apache.org/repos/asf/tomcat/tc7.0.x/trunk/java/org/apache/catalina/servlets/DefaultServlet.java 7 | http://svn.apache.org/repos/asf/tomcat/tc7.0.x/trunk/java/javax/servlet/http/HttpServlet.java 8 | 9 | webdav go partially 10 | https://github.com/bradfitz/camlistore/blob/master/old/camwebdav/main.go 11 | https://github.com/adrianuswarmenhoven/XBMC-BlastDav/blob/master/main.go 12 | 13 | go http extension examples 14 | http://godoc.org/net/http#_example_FileServer--StripPrefix 15 | http://godoc.org/code.google.com/p/go.net/websocket 16 | 17 | */ 18 | package webdav 19 | -------------------------------------------------------------------------------- /example/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/der-antikeks/go-webdav" 7 | ) 8 | 9 | func main() { 10 | fs, err := webdav.Dial("http://localhost:8080/webdav") 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | defer fs.Close() 15 | 16 | f, err := fs.Open(".") 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | defer f.Close() 21 | 22 | fi, err := f.Readdir(0) 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | for _, i := range fi { 28 | name := i.Name() 29 | if i.IsDir() { 30 | name += "/" 31 | } 32 | 33 | log.Println(name) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/der-antikeks/go-webdav" 10 | ) 11 | 12 | var ( 13 | path = "./tmp" 14 | ) 15 | 16 | func main() { 17 | os.Mkdir(path, os.ModeDir) 18 | 19 | // http.StripPrefix is not working, webdav.Server has no knowledge 20 | // of stripped component, but needs for COPY/MOVE methods. 21 | // Destination path is supplied as header and needs to be stripped. 22 | http.Handle("/webdav/", &webdav.Server{ 23 | Fs: webdav.Dir(path), 24 | TrimPrefix: "/webdav/", 25 | Listings: true, 26 | }) 27 | 28 | http.HandleFunc("/", index) 29 | 30 | log.Println("Listening on http://127.0.0.1:8080") 31 | log.Fatal(http.ListenAndServe(":8080", nil)) 32 | } 33 | 34 | func index(w http.ResponseWriter, r *http.Request) { 35 | fmt.Fprintf(w, "Hello, %q\n", r.URL.Path) 36 | } 37 | -------------------------------------------------------------------------------- /filesystem.go: -------------------------------------------------------------------------------- 1 | package webdav 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // A FileSystem implements access to a collection of named files. 12 | // The elements in a file path are separated by slash ('/', U+002F) 13 | // characters, regardless of host operating system convention. 14 | type FileSystem interface { 15 | Open(name string) (File, error) 16 | Create(name string) (File, error) 17 | Mkdir(path string) error 18 | Remove(name string) error 19 | } 20 | 21 | // A File is returned by a FileSystem's Open and Create method and can 22 | // be served by the FileServer implementation. 23 | type File interface { 24 | Stat() (os.FileInfo, error) 25 | Readdir(count int) ([]os.FileInfo, error) 26 | 27 | Read([]byte) (int, error) 28 | Write(p []byte) (n int, err error) 29 | Seek(offset int64, whence int) (int64, error) 30 | Close() error 31 | 32 | /* TODO: needed? 33 | Chdir() error 34 | Chmod(mode FileMode) error 35 | Chown(uid, gid int) error 36 | */ 37 | } 38 | 39 | // A Dir implements webdav.FileSystem using the native file 40 | // system restricted to a specific directory tree. 41 | // 42 | // An empty Dir is treated as ".". 43 | type Dir string 44 | 45 | func (d Dir) sanitizePath(name string) (string, error) { 46 | if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 || 47 | strings.Contains(name, "\x00") { 48 | return "", ErrInvalidCharPath 49 | } 50 | 51 | dir := string(d) 52 | if dir == "" { 53 | dir = "." 54 | } 55 | 56 | return filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))), nil 57 | } 58 | 59 | func (d Dir) Open(name string) (File, error) { 60 | p, err := d.sanitizePath(name) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | f, err := os.Open(p) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return f, nil 70 | } 71 | 72 | func (d Dir) Create(name string) (File, error) { 73 | p, err := d.sanitizePath(name) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | f, err := os.Create(p) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return f, nil 83 | } 84 | 85 | // Mkdir creates a new directory with the specified name 86 | func (d Dir) Mkdir(name string) error { 87 | p, err := d.sanitizePath(name) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return os.Mkdir(p, os.ModePerm) 93 | } 94 | 95 | func (d Dir) Remove(name string) error { 96 | p, err := d.sanitizePath(name) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | return os.Remove(p) 102 | } 103 | 104 | // mockup zero content file aka only header 105 | type emptyFile struct{} 106 | 107 | func (e emptyFile) Read(p []byte) (n int, err error) { 108 | return 0, io.EOF 109 | } 110 | 111 | func (e emptyFile) Seek(offset int64, whence int) (ret int64, err error) { 112 | return 0, nil 113 | } 114 | -------------------------------------------------------------------------------- /lock.go: -------------------------------------------------------------------------------- 1 | package webdav 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Lock struct { 11 | uri string 12 | creator string 13 | owner string 14 | depth int 15 | timeout TimeOut 16 | typ string 17 | scope string 18 | token string 19 | Modified time.Time 20 | } 21 | 22 | func NewLock(uri, creator, owner string) *Lock { 23 | return &Lock{ 24 | uri, 25 | creator, 26 | owner, 27 | 0, 28 | 0, 29 | "write", 30 | "exclusive", 31 | generateToken(), 32 | time.Now(), 33 | } 34 | } 35 | 36 | // parse a lock from a http request 37 | func ParseLockString(body string) (*Lock, error) { 38 | node, err := NodeFromXmlString(body) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | if node == nil { 44 | return nil, errors.New("no found node") 45 | } 46 | 47 | lock := new(Lock) 48 | 49 | if node.Name.Local != "lockinfo" { 50 | node = node.FirstChildren("lockinfo") 51 | } 52 | if node == nil { 53 | return nil, errors.New("not lockinfo element") 54 | } 55 | 56 | lock.scope = node.FirstChildren("lockscope").Children[0].Name.Local 57 | 58 | lock.typ = node.FirstChildren("locktype").Children[0].Name.Local 59 | 60 | lock.owner = node.FirstChildren("owner").Children[0].Value 61 | 62 | return lock, nil 63 | } 64 | 65 | func (lock *Lock) Refresh() { 66 | lock.Modified = time.Now() 67 | } 68 | 69 | func (lock *Lock) IsValid() bool { 70 | return time.Duration(lock.timeout) > time.Now().Sub(lock.Modified) 71 | } 72 | 73 | func (lock *Lock) GetTimeout() TimeOut { 74 | return lock.timeout 75 | } 76 | 77 | func (lock *Lock) SetTimeout(timeout time.Duration) { 78 | lock.timeout = TimeOut(timeout) 79 | lock.Modified = time.Now() 80 | } 81 | 82 | func (lock *Lock) asXML(namespace string, discover bool) string { 83 | //owner_str = lock.owner 84 | //owner_str = "".join([node.toxml() for node in self.owner[0].childNodes]) 85 | 86 | base := fmt.Sprintf(`<%[1]s:activelock> 87 | <%[1]s:locktype><%[1]s:%[2]s/> 88 | <%[1]s:lockscope><%[1]s:%[3]s/> 89 | <%[1]s:depth>%[4]d 90 | <%[1]s:owner>%[5]s 91 | <%[1]s:timeout>%[6]s 92 | <%[1]s:locktoken> 93 | <%[1]s:href>opaquelocktoken:%[7]s 94 | 95 | <%[1]s:lockroot> 96 | <%[1]s:href>%[8]s 97 | 98 | 99 | `, strings.Trim(namespace, ":"), 100 | lock.typ, 101 | lock.scope, 102 | lock.depth, 103 | lock.owner, 104 | lock.GetTimeout(), 105 | lock.token, 106 | lock.uri, 107 | ) 108 | 109 | if discover { 110 | return base 111 | } 112 | 113 | return fmt.Sprintf(` 114 | 115 | 116 | %s 117 | 118 | `, base) 119 | } 120 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package webdav 2 | 3 | import ( 4 | "encoding/xml" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | type Node struct { 10 | Name xml.Name 11 | Attr []xml.Attr 12 | Children []*Node 13 | Value string 14 | Parent *Node 15 | } 16 | 17 | func NodeFromXmlString(xmls string) (*Node, error) { 18 | rd := strings.NewReader(xmls) 19 | return NodeFromXml(rd) 20 | } 21 | 22 | func NodeFromXml(r io.Reader) (*Node, error) { 23 | var cur, parent *Node 24 | 25 | decoder := xml.NewDecoder(r) 26 | for { 27 | token, err := decoder.Token() 28 | if err != nil { 29 | if err != io.EOF { 30 | return nil, err 31 | } 32 | break 33 | } 34 | 35 | switch tok := token.(type) { 36 | case xml.StartElement: 37 | parent = cur 38 | 39 | // if tok.Name.Space != "DAV:" { 40 | if tok.Name.Space == "" { 41 | return nil, ErrMalformedXml 42 | } 43 | 44 | cur = &Node{ 45 | Name: tok.Name, 46 | Attr: tok.Attr, 47 | Parent: parent, 48 | } 49 | 50 | if parent != nil { 51 | parent.Children = append(parent.Children, cur) 52 | } 53 | case xml.CharData: 54 | if cur != nil { 55 | cur.Value = string(tok) 56 | } 57 | case xml.EndElement: 58 | if cur.Parent == nil { 59 | return cur, nil 60 | } 61 | cur = cur.Parent 62 | default: 63 | //log.Printf("%T", tok) 64 | } 65 | } 66 | 67 | return cur, nil 68 | } 69 | 70 | func (n Node) HasChildren(name string) bool { 71 | for _, v := range n.Children { 72 | if v.Name.Local == name { 73 | return true 74 | } 75 | } 76 | return false 77 | } 78 | 79 | func (n *Node) GetChildrens(name string) []*Node { 80 | if name == "*" { 81 | return n.Children 82 | } 83 | 84 | var ret []*Node 85 | 86 | for _, v := range n.Children { 87 | if v.Name.Local == name { 88 | ret = append(ret, v) 89 | } 90 | } 91 | 92 | return ret 93 | } 94 | 95 | func (n *Node) FirstChildren(name string) *Node { 96 | if name == "*" && len(n.Children) > 0 { 97 | return n.Children[0] 98 | } 99 | 100 | for _, v := range n.Children { 101 | if v.Name.Local == name { 102 | return v 103 | } 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func (n *Node) String() string { 110 | r := "<" + n.Name.Local + " xmlns=\"" + n.Name.Space + "\">\n" 111 | 112 | if len(n.Children) > 0 { 113 | for _, v := range n.Children { 114 | r += v.String() + "\n" 115 | } 116 | } else if len(n.Value) > 0 { 117 | r += n.Value + "\n" 118 | } 119 | 120 | r += "<\\" + n.Name.Local + ">" 121 | 122 | return r 123 | } 124 | -------------------------------------------------------------------------------- /node_test.go: -------------------------------------------------------------------------------- 1 | package webdav 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | var txt = ` 10 | 11 | 12 | 13 | 14 | mailto:xiaolunwen@gmail.com 15 | 16 | ` 17 | 18 | func TestNodeFromXml(t *testing.T) { 19 | rd := strings.NewReader(txt) 20 | node, err := NodeFromXml(rd) 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | 25 | fmt.Println("node.Name.Local", node.Name.Local) 26 | fmt.Println("node.Name.Space", node.Name.Space) 27 | fmt.Println(node.FirstChildren("owner")) 28 | } 29 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package webdav 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "math/rand" 9 | "mime" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "os/exec" 14 | "path" 15 | "path/filepath" 16 | "strconv" 17 | "strings" 18 | "time" 19 | 20 | qlog "github.com/qiniu/log" 21 | ) 22 | 23 | var logger *qlog.Logger 24 | 25 | func newLogger(execDir string) { 26 | logPath := execDir + "/log/webdav.log" 27 | os.MkdirAll(path.Dir(logPath), os.ModePerm) 28 | 29 | f, err := os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.ModePerm) 30 | if err != nil { 31 | qlog.Fatal(err) 32 | } 33 | 34 | logger = qlog.New(f, "", qlog.Ldate|qlog.Ltime) 35 | logger.Info("Start logging webdav...") 36 | } 37 | 38 | func ExecDir() (string, error) { 39 | file, err := exec.LookPath(os.Args[0]) 40 | if err != nil { 41 | return "", err 42 | } 43 | p, err := filepath.Abs(file) 44 | if err != nil { 45 | return "", err 46 | } 47 | return path.Dir(strings.Replace(p, "\\", "/", -1)), nil 48 | } 49 | 50 | func init() { 51 | exePath, err := ExecDir() 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | newLogger(exePath) 57 | } 58 | 59 | func Handler(root FileSystem) http.Handler { 60 | return &Server{Fs: root} 61 | } 62 | 63 | type Server struct { 64 | // trimmed path prefix 65 | TrimPrefix string 66 | 67 | // files are readonly? 68 | ReadOnly bool 69 | 70 | // generate directory listings? 71 | Listings bool 72 | 73 | // access to a collection of named files 74 | Fs FileSystem 75 | 76 | tokens_to_lock map[string]*Lock 77 | 78 | path_to_token map[string]string 79 | } 80 | 81 | func generateToken() string { 82 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 83 | return fmt.Sprintf("%d-%d-00105A989226:%d", 84 | r.Int31(), r.Int31(), time.Now().UnixNano()) 85 | } 86 | 87 | func NewServer(dir, prefix string, listDir bool) *Server { 88 | return &Server{ 89 | Fs: Dir(dir), 90 | TrimPrefix: prefix, 91 | Listings: listDir, 92 | tokens_to_lock: make(map[string]*Lock), 93 | path_to_token: make(map[string]string), 94 | } 95 | } 96 | 97 | var ( 98 | PullMethods = map[string]bool{ 99 | "OPTIONS": true, 100 | "GET": true, 101 | "HEAD": true, 102 | "PROPFIND": true} 103 | 104 | PushMethods = map[string]bool{ 105 | "POST": true, 106 | "DELETE": true, 107 | "PUT": true, 108 | "PROPPATCH": true, 109 | "MKCOL": true, 110 | "COPY": true, 111 | "MOVE": true, 112 | "LOCK": true, 113 | "UNLOCK": true, 114 | } 115 | ) 116 | 117 | func IsPullMethod(method string) bool { 118 | _, ok := PullMethods[method] 119 | return ok 120 | } 121 | 122 | func IsPushMethod(method string) bool { 123 | _, ok := PushMethods[method] 124 | return ok 125 | } 126 | 127 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 128 | //log.Println("DAV:", r.RemoteAddr, r.Method, r.URL) 129 | 130 | switch r.Method { 131 | case "OPTIONS": 132 | s.doOptions(w, r) 133 | 134 | case "GET": 135 | s.doGet(w, r) 136 | case "HEAD": 137 | s.doHead(w, r) 138 | case "POST": 139 | s.doPost(w, r) 140 | case "DELETE": 141 | s.doDelete(w, r) 142 | case "PUT": 143 | s.doPut(w, r) 144 | 145 | case "PROPFIND": 146 | s.doPropfind(w, r) 147 | case "PROPPATCH": 148 | s.doProppatch(w, r) 149 | case "MKCOL": 150 | s.doMkcol(w, r) 151 | case "COPY": 152 | s.doCopy(w, r) 153 | case "MOVE": 154 | s.doMove(w, r) 155 | 156 | case "LOCK": 157 | s.doLock(w, r) 158 | case "UNLOCK": 159 | s.doUnlock(w, r) 160 | 161 | default: 162 | qlog.Error("DAV:", "unknown method", r.Method) 163 | w.WriteHeader(StatusBadRequest) 164 | } 165 | } 166 | 167 | func (s *Server) methodsAllowed(path string) string { 168 | if !s.pathExists(path) { 169 | return "OPTIONS, MKCOL, PUT, LOCK" 170 | } 171 | 172 | allowed := "OPTIONS, GET, HEAD, POST, DELETE, TRACE, PROPPATCH, COPY, MOVE, LOCK, UNLOCK" 173 | 174 | if s.Listings { 175 | allowed += ", PROPFIND" 176 | } 177 | 178 | if s.pathIsDirectory(path) { 179 | allowed += ", PUT" 180 | } 181 | 182 | return allowed 183 | } 184 | 185 | // convert request url to path 186 | func (s *Server) url2path(u *url.URL) string { 187 | if u.Path == "" { 188 | return "/" 189 | } 190 | 191 | if p := strings.TrimPrefix(u.Path, s.TrimPrefix); len(p) < len(u.Path) { 192 | return strings.Trim(p, "/") 193 | } 194 | 195 | return "/" 196 | } 197 | 198 | // convert path to url 199 | func (s *Server) path2url(p string) *url.URL { 200 | return &url.URL{Path: path.Join("/", s.TrimPrefix, p)} 201 | } 202 | 203 | // does path exists? 204 | func (s *Server) pathExists(path string) bool { 205 | f, err := s.Fs.Open(path) 206 | if err != nil { 207 | return false 208 | } 209 | defer f.Close() 210 | 211 | return true 212 | } 213 | 214 | // is path a directory? 215 | func (s *Server) pathIsDirectory(path string) bool { 216 | f, err := s.Fs.Open(path) 217 | if err != nil { 218 | return false 219 | } 220 | defer f.Close() 221 | 222 | fi, err := f.Stat() 223 | if err != nil { 224 | return false 225 | } 226 | 227 | return fi.IsDir() 228 | } 229 | 230 | func (s *Server) directoryContents(path string) []string { 231 | f, err := s.Fs.Open(path) 232 | if err != nil { 233 | return nil 234 | } 235 | defer f.Close() 236 | 237 | fi, err := f.Readdir(0) 238 | if err != nil { 239 | return nil 240 | } 241 | 242 | ret := make([]string, len(fi)) 243 | for k, i := range fi { 244 | name := i.Name() 245 | if i.IsDir() { 246 | name += "/" 247 | } 248 | ret[k] = name 249 | } 250 | 251 | return ret 252 | } 253 | 254 | // is path in request locked? 255 | func (s *Server) isLockedRequest(r *http.Request) bool { 256 | return s.isLocked( 257 | s.url2path(r.URL), 258 | r.Header.Get("If") /*+r.Header.Get("Lock-Token")*/) 259 | } 260 | 261 | // is path locked? 262 | func (s *Server) isLocked(path, ifHeader string) bool { 263 | token, ok := s.path_to_token[path] 264 | if !ok { 265 | return false 266 | } 267 | 268 | if ifHeader == "" { 269 | return true 270 | } 271 | 272 | taglist := IfParser(ifHeader) 273 | found := false 274 | for _, tag := range taglist { 275 | for _, listitem := range tag.list { 276 | token = tokenFinder(listitem) 277 | if (token != "") && 278 | s.hasLock(token) && 279 | (s.getLock(token).uri == path) { 280 | found = true 281 | break 282 | } 283 | } 284 | if found { 285 | break 286 | } 287 | } 288 | 289 | return !found 290 | } 291 | 292 | func (s *Server) hasLock(token string) bool { 293 | _, ok := s.tokens_to_lock[token] 294 | return ok 295 | } 296 | 297 | func (s *Server) getToken(uri string) string { 298 | return s.path_to_token[uri] 299 | } 300 | 301 | func (s *Server) getLock(token string) *Lock { 302 | return s.tokens_to_lock[token] 303 | } 304 | 305 | func (s *Server) delLock(token string) { 306 | if lock, ok := s.tokens_to_lock[token]; ok { 307 | delete(s.path_to_token, lock.uri) 308 | delete(s.tokens_to_lock, token) 309 | } 310 | } 311 | 312 | func (s *Server) setLock(lock *Lock) { 313 | s.tokens_to_lock[lock.token] = lock 314 | s.path_to_token[lock.uri] = lock.token 315 | } 316 | 317 | func (s *Server) lockResource(path string) { 318 | // TODO 319 | } 320 | 321 | func (s *Server) unlockResource(path string) { 322 | // TODO 323 | } 324 | 325 | // The PROPFIND method retrieves properties defined on the resource identified by the Request-URI 326 | // http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND 327 | func (s *Server) doPropfind(w http.ResponseWriter, r *http.Request) { 328 | if !s.Listings { 329 | w.Header().Set("Allow", s.methodsAllowed(s.url2path(r.URL))) 330 | w.WriteHeader(StatusMethodNotAllowed) 331 | return 332 | } 333 | 334 | depth := r.Header.Get("Depth") 335 | switch depth { 336 | case "0", "1": 337 | case "", "infinity": 338 | // treat as infinity if no depth header was included 339 | // disable infinity for performance and security concerns 340 | // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.1.1 341 | w.WriteHeader(StatusForbidden) 342 | return 343 | default: 344 | w.WriteHeader(StatusBadRequest) 345 | return 346 | } 347 | 348 | var propnames bool 349 | var properties []string 350 | var includes []string 351 | 352 | if r.ContentLength > 0 { 353 | propfind, err := NodeFromXml(r.Body) 354 | if err != nil { 355 | w.WriteHeader(StatusBadRequest) 356 | return 357 | } 358 | 359 | if propfind.Name.Local != "propfind" { 360 | w.WriteHeader(StatusBadRequest) 361 | return 362 | } 363 | 364 | // find by property 365 | // http://www.webdav.org/specs/rfc4918.html#dav.properties 366 | if propfind.HasChildren("prop") { 367 | prop := propfind.FirstChildren("prop") 368 | for _, p := range prop.GetChildrens("*") { 369 | properties = append(properties, p.Name.Local) 370 | } 371 | } 372 | 373 | // find property names 374 | if propfind.HasChildren("propname") { 375 | propnames = true 376 | } 377 | 378 | // find all properties 379 | if propfind.HasChildren("allprop") { 380 | properties = []string{ 381 | "creationdate", "displayname", 382 | "getcontentlanguage", "getcontentlength", 383 | "getcontenttype", "getetag", 384 | "getlastmodified", "lockdiscovery", 385 | "resourcetype", "supportedlock", 386 | } 387 | 388 | if propfind.HasChildren("include") { 389 | for _, i := range propfind.GetChildrens("include") { 390 | includes = append(includes, i.Name.Local) 391 | } 392 | } 393 | } 394 | } 395 | 396 | path := s.url2path(r.URL) 397 | if !s.pathExists(path) { 398 | http.Error(w, path, StatusNotFound) 399 | // TODO: if locked (parent locked?) return multistatus with locked error as propstat 400 | return 401 | } 402 | 403 | paths := []string{path} 404 | if depth == "1" { 405 | // fetch all files if directory 406 | // TODO: respect []includes 407 | 408 | if s.pathIsDirectory(path) { 409 | for _, p := range s.directoryContents(path) { 410 | paths = append(paths, path+"/"+p) 411 | } 412 | } 413 | } 414 | 415 | buf := new(bytes.Buffer) 416 | buf.WriteString(``) 417 | buf.WriteString(``) 418 | 419 | // TODO: https? 420 | abs := "http://" + r.Host + s.TrimPrefix 421 | 422 | for _, p := range paths { 423 | // TODO 424 | // test locks/ authorization 425 | // if properties, show only given properties, else all 426 | // if propnames, return names of properties, else names and values 427 | 428 | propertiesNotFound := []string{} 429 | 430 | f, _ := s.Fs.Open(p) 431 | defer f.Close() 432 | fi, _ := f.Stat() 433 | 434 | buf.WriteString(``) 435 | buf.WriteString(`` + abs + "/" + p + ``) 436 | buf.WriteString(``) 437 | { 438 | buf.WriteString(``) 439 | { 440 | // TODO: make less ugly 441 | for _, prop := range properties { 442 | 443 | switch prop { 444 | case "creationdate": 445 | if propnames { 446 | buf.WriteString(`<` + prop + `/>`) 447 | } else { 448 | buf.WriteString(`<` + prop + `>`) 449 | buf.WriteString(fi.ModTime().Format("2006-01-02T15:04:05Z07:00")) 450 | buf.WriteString(``) 451 | } 452 | case "getcontentlanguage": 453 | if propnames { 454 | buf.WriteString(`<` + prop + `/>`) 455 | } else { 456 | buf.WriteString(`<` + prop + `>`) 457 | buf.WriteString(`en`) 458 | buf.WriteString(``) 459 | } 460 | case "getcontentlength": 461 | if fi.IsDir() { 462 | } else if propnames { 463 | buf.WriteString(`<` + prop + `/>`) 464 | } else { 465 | buf.WriteString(`<` + prop + `>`) 466 | buf.WriteString(strconv.FormatInt(int64(fi.Size()), 10)) 467 | buf.WriteString(``) 468 | } 469 | case "getcontenttype": 470 | if fi.IsDir() { 471 | } else if propnames { 472 | buf.WriteString(`<` + prop + `/>`) 473 | } else { 474 | buf.WriteString(`<` + prop + `>`) 475 | buf.WriteString(mime.TypeByExtension(filepath.Ext(fi.Name()))) 476 | buf.WriteString(``) 477 | } 478 | case "getlastmodified": 479 | if fi.IsDir() { 480 | } else if propnames { 481 | buf.WriteString(`<` + prop + `/>`) 482 | } else { 483 | buf.WriteString(`<` + prop + `>`) 484 | buf.WriteString(fi.ModTime().Format("Mon, 02 Jan 2006 15:04:05 MST")) 485 | buf.WriteString(``) 486 | } 487 | case "resourcetype": 488 | if propnames || !fi.IsDir() { 489 | // ZODO: reson for all the ugliness 490 | buf.WriteString(`<` + prop + `/>`) 491 | } else { 492 | buf.WriteString(`<` + prop + `>`) 493 | buf.WriteString(``) 494 | buf.WriteString(``) 495 | } 496 | case "displayname": 497 | if propnames { 498 | buf.WriteString(`<` + prop + `/>`) 499 | } else { 500 | buf.WriteString(`<` + prop + `>`) 501 | buf.WriteString(fi.Name()) 502 | buf.WriteString(``) 503 | } 504 | case "supportedlock": 505 | if propnames { 506 | buf.WriteString(`<` + prop + `/>`) 507 | } else { 508 | buf.WriteString(`<` + prop + `>`) 509 | buf.WriteString(``) 510 | buf.WriteString(``) 511 | buf.WriteString(``) 512 | } 513 | 514 | // TODO: implement later at locks-stage 515 | // case "getetag": // not for dir 516 | // case "lockdiscovery": 517 | default: 518 | propertiesNotFound = append(propertiesNotFound, prop) 519 | } 520 | } 521 | } 522 | buf.WriteString(``) 523 | buf.WriteString(`HTTP/1.1 200 OK`) 524 | } 525 | buf.WriteString(``) 526 | 527 | if len(propertiesNotFound) > 0 { 528 | buf.WriteString(``) 529 | { 530 | buf.WriteString(``) 531 | { 532 | for _, prop := range propertiesNotFound { 533 | buf.WriteString(`<` + prop + `/>`) 534 | } 535 | } 536 | buf.WriteString(``) 537 | buf.WriteString(`HTTP/1.1 404 ` + StatusText(404) + ``) 538 | } 539 | buf.WriteString(``) 540 | } 541 | 542 | buf.WriteString(``) 543 | } 544 | 545 | buf.WriteString(``) 546 | 547 | w.WriteHeader(StatusMulti) 548 | w.Header().Set("Content-Length", string(buf.Len())) 549 | w.Header().Set("Content-Type", "application/xml; charset=utf-8") 550 | 551 | buf.WriteTo(w) 552 | // TODO: possible write error is suppressed 553 | } 554 | 555 | // http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH 556 | func (s *Server) doProppatch(w http.ResponseWriter, r *http.Request) { 557 | if s.ReadOnly { 558 | w.WriteHeader(StatusForbidden) 559 | return 560 | } 561 | 562 | if s.isLockedRequest(r) { 563 | w.WriteHeader(StatusLocked) 564 | return 565 | } 566 | 567 | // TODO: proppatch 568 | w.WriteHeader(StatusNotImplemented) 569 | } 570 | 571 | // http://www.webdav.org/specs/rfc4918.html#METHOD_MKCOL 572 | func (s *Server) doMkcol(w http.ResponseWriter, r *http.Request) { 573 | if s.ReadOnly { 574 | w.WriteHeader(StatusForbidden) 575 | return 576 | } 577 | 578 | if s.isLockedRequest(r) { 579 | w.WriteHeader(StatusLocked) 580 | return 581 | } 582 | 583 | path := s.url2path(r.URL) 584 | if s.pathExists(path) { 585 | w.Header().Set("Allow", s.methodsAllowed(s.url2path(r.URL))) 586 | w.WriteHeader(StatusMethodNotAllowed) 587 | return 588 | } 589 | 590 | // MKCOL may contain messagebody, precise behavior is undefined 591 | if r.ContentLength > 0 { 592 | _, err := NodeFromXml(r.Body) 593 | if err != nil { 594 | w.WriteHeader(StatusBadRequest) 595 | return 596 | } 597 | 598 | w.WriteHeader(StatusUnsupportedMediaType) 599 | return 600 | } 601 | 602 | if err := s.Fs.Mkdir(path); err != nil { 603 | w.WriteHeader(StatusConflict) 604 | return 605 | } 606 | 607 | w.WriteHeader(StatusCreated) 608 | s.unlockResource(path) 609 | } 610 | 611 | // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.4 612 | func (s *Server) doGet(w http.ResponseWriter, r *http.Request) { 613 | s.serveResource(w, r, true) 614 | } 615 | 616 | // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.4 617 | func (s *Server) doHead(w http.ResponseWriter, r *http.Request) { 618 | s.serveResource(w, r, false) 619 | } 620 | 621 | // http://www.webdav.org/specs/rfc4918.html#METHOD_POST 622 | func (s *Server) doPost(w http.ResponseWriter, r *http.Request) { 623 | s.doGet(w, r) 624 | } 625 | 626 | func (s *Server) serveResource(w http.ResponseWriter, r *http.Request, serveContent bool) { 627 | path := s.url2path(r.URL) 628 | 629 | f, err := s.Fs.Open(path) 630 | if err != nil { 631 | http.Error(w, r.RequestURI, StatusNotFound) 632 | return 633 | } 634 | defer f.Close() 635 | 636 | // TODO: what if path is collection? 637 | 638 | fi, err := f.Stat() 639 | if err != nil { 640 | http.Error(w, r.RequestURI, StatusNotFound) 641 | return 642 | } 643 | modTime := fi.ModTime() 644 | 645 | if serveContent { 646 | http.ServeContent(w, r, path, modTime, f) 647 | } else { 648 | // TODO: better way to send only head 649 | http.ServeContent(w, r, path, modTime, emptyFile{}) 650 | } 651 | } 652 | 653 | // http://www.webdav.org/specs/rfc4918.html#METHOD_DELETE 654 | func (s *Server) doDelete(w http.ResponseWriter, r *http.Request) { 655 | if s.ReadOnly { 656 | w.WriteHeader(StatusForbidden) 657 | return 658 | } 659 | 660 | if s.isLockedRequest(r) { 661 | w.WriteHeader(StatusLocked) 662 | return 663 | } 664 | 665 | s.deleteResource(s.url2path(r.URL), w, r, true) 666 | } 667 | 668 | func (s *Server) deleteResource(path string, w http.ResponseWriter, r *http.Request, setStatus bool) bool { 669 | ifHeader := r.Header.Get("If") 670 | lockToken := r.Header.Get("Lock-Token") 671 | 672 | if s.isLocked(path, ifHeader+lockToken) { 673 | w.WriteHeader(StatusLocked) 674 | return false 675 | } 676 | 677 | if !s.pathExists(path) { 678 | w.WriteHeader(StatusNotFound) 679 | return false 680 | } 681 | 682 | if !s.pathIsDirectory(path) { 683 | if err := s.Fs.Remove(path); err != nil { 684 | w.WriteHeader(StatusInternalServerError) 685 | return false 686 | } 687 | } else { 688 | // http://www.webdav.org/specs/rfc4918.html#delete-collections 689 | errors := map[string]int{} 690 | s.deleteCollection(path, w, r, errors) 691 | 692 | if err := s.Fs.Remove(path); err != nil { 693 | errors[path] = StatusInternalServerError 694 | } 695 | 696 | if len(errors) != 0 { 697 | // send multistatus 698 | abs := r.RequestURI 699 | 700 | buf := new(bytes.Buffer) 701 | buf.WriteString(``) 702 | buf.WriteString(``) 703 | 704 | for p, e := range errors { 705 | buf.WriteString(``) 706 | buf.WriteString(`` + abs + p + ``) 707 | buf.WriteString(`HTTP/1.1 ` + string(e) + ` ` + StatusText(e) + ``) 708 | buf.WriteString(``) 709 | } 710 | 711 | buf.WriteString(``) 712 | 713 | w.WriteHeader(StatusMulti) 714 | w.Header().Set("Content-Length", string(buf.Len())) 715 | w.Header().Set("Content-Type", "application/xml; charset=utf-8") 716 | buf.WriteTo(w) 717 | 718 | return false 719 | } 720 | } 721 | 722 | if setStatus { 723 | w.WriteHeader(StatusNoContent) 724 | } 725 | return true 726 | } 727 | 728 | func (s *Server) deleteCollection(path string, w http.ResponseWriter, r *http.Request, errors map[string]int) { 729 | ifHeader := r.Header.Get("If") 730 | lockToken := r.Header.Get("Lock-Token") 731 | 732 | for _, p := range s.directoryContents(path) { 733 | p = path + "/" + p 734 | 735 | if s.isLocked(p, ifHeader+lockToken) { 736 | errors[p] = StatusLocked 737 | } else { 738 | if s.pathIsDirectory(p) { 739 | s.deleteCollection(p, w, r, errors) 740 | } 741 | 742 | if err := s.Fs.Remove(p); err != nil { 743 | errors[p] = StatusInternalServerError 744 | } 745 | } 746 | } 747 | 748 | } 749 | 750 | // http://www.webdav.org/specs/rfc4918.html#METHOD_PUT 751 | func (s *Server) doPut(w http.ResponseWriter, r *http.Request) { 752 | if s.ReadOnly { 753 | w.WriteHeader(StatusForbidden) 754 | return 755 | } 756 | 757 | if s.isLockedRequest(r) { 758 | w.WriteHeader(StatusLocked) 759 | return 760 | } 761 | 762 | path := s.url2path(r.URL) 763 | 764 | if s.pathIsDirectory(path) { 765 | // use MKCOL instead 766 | w.WriteHeader(StatusMethodNotAllowed) 767 | return 768 | } 769 | 770 | exists := s.pathExists(path) 771 | 772 | // TODO: content range / partial put 773 | 774 | // truncate file if exists 775 | file, err := s.Fs.Create(path) 776 | if err != nil { 777 | w.WriteHeader(StatusConflict) 778 | return 779 | } 780 | defer file.Close() 781 | 782 | if _, err := io.Copy(file, r.Body); err != nil { 783 | w.WriteHeader(StatusConflict) 784 | } else { 785 | if exists { 786 | w.WriteHeader(StatusNoContent) 787 | } else { 788 | w.WriteHeader(StatusCreated) 789 | } 790 | } 791 | 792 | s.unlockResource(path) 793 | } 794 | 795 | // http://www.webdav.org/specs/rfc4918.html#METHOD_COPY 796 | func (s *Server) doCopy(w http.ResponseWriter, r *http.Request) { 797 | if s.ReadOnly { 798 | w.WriteHeader(StatusForbidden) 799 | return 800 | } 801 | 802 | s.copyResource(w, r) 803 | } 804 | 805 | // http://www.webdav.org/specs/rfc4918.html#METHOD_MOVE 806 | func (s *Server) doMove(w http.ResponseWriter, r *http.Request) { 807 | if s.ReadOnly { 808 | w.WriteHeader(StatusForbidden) 809 | return 810 | } 811 | 812 | if s.isLockedRequest(r) { 813 | w.WriteHeader(StatusLocked) 814 | return 815 | } 816 | 817 | if s.copyResource(w, r) { 818 | // TODO: duplicate http-header sent? 819 | s.deleteResource(s.url2path(r.URL), w, r, false) 820 | } 821 | } 822 | 823 | func (s *Server) copyResource(w http.ResponseWriter, r *http.Request) bool { 824 | dest := r.Header.Get("Destination") 825 | if dest == "" { 826 | w.WriteHeader(StatusBadRequest) 827 | return false 828 | } 829 | 830 | d, err := url.Parse(dest) 831 | if err != nil { 832 | w.WriteHeader(StatusBadRequest) 833 | return false 834 | } 835 | // TODO: normalize dest? 836 | dest = s.url2path(d) 837 | source := s.url2path(r.URL) 838 | 839 | // source equals destination 840 | if source == dest { 841 | w.WriteHeader(StatusForbidden) 842 | return false 843 | } 844 | 845 | // destination must be same server/namespace as source 846 | if d.Host != r.Host || 847 | !strings.HasPrefix(d.Path, s.TrimPrefix) || 848 | !strings.HasPrefix(r.URL.Path, s.TrimPrefix) { 849 | 850 | w.WriteHeader(StatusBadGateway) 851 | return false 852 | } 853 | 854 | // TODO: needs to be tested? should be catched with error at CopyFile returning StatusConflict 855 | // currently only at depth=0 or non-collection copy 856 | /* 857 | parentDest := dest[:strings.LastIndex(dest, "/")] 858 | if !s.pathExists(parentDest) { 859 | w.WriteHeader(StatusConflict) 860 | return false 861 | } 862 | */ 863 | 864 | overwrite := r.Header.Get("Overwrite") != "F" 865 | exists := s.pathExists(dest) 866 | 867 | if overwrite { 868 | if exists { 869 | if !s.deleteResource(dest, w, r, false) { 870 | w.WriteHeader(StatusInternalServerError) 871 | return false 872 | } 873 | } 874 | } else { 875 | if exists { 876 | w.WriteHeader(StatusPreconditionFailed) 877 | return false 878 | } 879 | } 880 | 881 | if !s.pathIsDirectory(source) { 882 | if err := s.CopyFile(source, dest); err != nil { 883 | // TODO: always conflict? e.g. copy to non-existant path 884 | //w.WriteHeader(StatusInternalServerError) 885 | w.WriteHeader(StatusConflict) 886 | return false 887 | } 888 | } else if r.Header.Get("Depth") == "0" { 889 | // copy only collection, not its internal members 890 | // http://www.webdav.org/specs/rfc4918.html#copy.for.collections 891 | if err := s.Fs.Mkdir(dest); err != nil { 892 | w.WriteHeader(StatusConflict) 893 | return false 894 | } 895 | } else { 896 | // http://www.webdav.org/specs/rfc4918.html#copy.for.collections 897 | errors := map[string]int{} 898 | 899 | if err := s.Fs.Mkdir(dest); err != nil { 900 | errors[source] = StatusInternalServerError 901 | } 902 | 903 | s.copyCollection(source, dest, w, r, errors) 904 | 905 | if len(errors) != 0 { 906 | // send multistatus 907 | abs := r.RequestURI 908 | 909 | buf := new(bytes.Buffer) 910 | buf.WriteString(``) 911 | buf.WriteString(``) 912 | 913 | for p, e := range errors { 914 | buf.WriteString(``) 915 | buf.WriteString(`` + abs + p + ``) 916 | buf.WriteString(`HTTP/1.1 ` + string(e) + ` ` + StatusText(e) + ``) 917 | buf.WriteString(``) 918 | } 919 | 920 | buf.WriteString(``) 921 | 922 | w.WriteHeader(StatusMulti) 923 | w.Header().Set("Content-Length", string(buf.Len())) 924 | w.Header().Set("Content-Type", "application/xml; charset=utf-8") 925 | buf.WriteTo(w) 926 | 927 | return false 928 | } 929 | } 930 | 931 | // copy was successful 932 | if exists { 933 | w.WriteHeader(StatusNoContent) 934 | } else { 935 | w.WriteHeader(StatusCreated) 936 | } 937 | 938 | s.unlockResource(dest) 939 | return true 940 | } 941 | 942 | func (s *Server) CopyFile(source, dest string) error { 943 | // open source file 944 | fs, err := s.Fs.Open(source) 945 | if err != nil { 946 | return err 947 | } 948 | defer fs.Close() 949 | 950 | // open destination file 951 | fd, err := s.Fs.Create(dest) 952 | if err != nil { 953 | return err 954 | } 955 | defer fd.Close() 956 | 957 | // copy file contents 958 | if _, err := io.Copy(fd, fs); err != nil { 959 | return err 960 | } 961 | 962 | // TODO: copy file stats? http://www.webdav.org/specs/rfc4918.html#copy.for.properties 963 | 964 | return nil 965 | } 966 | 967 | func (s *Server) copyCollection(source, dest string, w http.ResponseWriter, r *http.Request, errors map[string]int) { 968 | ifHeader := r.Header.Get("If") 969 | lockToken := r.Header.Get("Lock-Token") 970 | 971 | for _, sub := range s.directoryContents(source) { 972 | ssub := source + "/" + sub 973 | dsub := dest + "/" + sub 974 | 975 | if s.isLocked(ssub, ifHeader+lockToken) { 976 | errors[ssub] = StatusLocked 977 | } else { 978 | if s.pathIsDirectory(ssub) { 979 | if err := s.Fs.Mkdir(dsub); err != nil { 980 | errors[ssub] = StatusInternalServerError 981 | } 982 | 983 | s.copyCollection(ssub, dsub, w, r, errors) 984 | } else { 985 | if err := s.CopyFile(ssub, dsub); err != nil { 986 | errors[ssub] = StatusInternalServerError 987 | } 988 | } 989 | } 990 | } 991 | } 992 | 993 | /*func (s *Server) _lock_unlock_parse(body string) (map[string]string, error) { 994 | node, err := NodeFromXmlString(body) 995 | if err != nil { 996 | return nil, err 997 | } 998 | 999 | data := make(map[string]string) 1000 | if node != nil { 1001 | if node.Name.Local != "lockinfo" { 1002 | node = node.FirstChildren("lockinfo") 1003 | } 1004 | if node == nil { 1005 | return nil, errors.New("not lockinfo element") 1006 | } 1007 | 1008 | data["lockscope"] = node.FirstChildren("lockscope").Children[0].Name.Local 1009 | 1010 | data["locktype"] = node.FirstChildren("locktype").Children[0].Name.Local 1011 | 1012 | data["lockowner"] = node.FirstChildren("owner").Children[0].Value[7:] 1013 | } 1014 | return data, nil 1015 | }*/ 1016 | 1017 | func (s *Server) _lock_unlock_create(lock *Lock, depth string) (string, string) { 1018 | //lock := &Lock{uri: uri, creator: creator} 1019 | iscollection := (lock.uri[len(lock.uri)-1] == '/') //# very dumb collection check 1020 | 1021 | result := "" 1022 | if depth == "infinity" && iscollection { 1023 | //# locking of children/collections not yet supported 1024 | //pass 1025 | } 1026 | 1027 | if !s.isLocked(lock.uri, "") { 1028 | s.setLock(lock) 1029 | } 1030 | 1031 | //# because we do not handle children we leave result empty 1032 | return lock.token, result 1033 | } 1034 | 1035 | /* 1036 | LOCK /workspace/webdav/proposal.doc HTTP/1.1 1037 | Host: example.com 1038 | Timeout: Infinite, Second-4100000000 1039 | Content-Type: application/xml; charset="utf-8" 1040 | Content-Length: xxxx 1041 | Authorization: Digest username="ejw", 1042 | realm="ejw@example.com", nonce="...", 1043 | uri="/workspace/webdav/proposal.doc", 1044 | response="...", opaque="..." 1045 | 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | http://example.org/~ejw/contact.html 1052 | 1053 | 1054 | */ 1055 | 1056 | func (s *Server) doLock(w http.ResponseWriter, r *http.Request) { 1057 | if s.ReadOnly { 1058 | w.WriteHeader(StatusForbidden) 1059 | return 1060 | } 1061 | 1062 | if s.isLockedRequest(r) { 1063 | w.WriteHeader(StatusLocked) 1064 | return 1065 | } 1066 | 1067 | //dc = self.IFACE_CLASS 1068 | 1069 | qlog.Info("LOCKing resource %s", r.Header) 1070 | 1071 | bbody, err := ioutil.ReadAll(r.Body) 1072 | if err != nil { 1073 | fmt.Println(err) 1074 | w.WriteHeader(500) 1075 | return 1076 | } 1077 | var body = string(bbody) 1078 | 1079 | depth := "infinity" 1080 | if r.Header.Get("Depth") != "" { 1081 | depth = r.Header.Get("Depth") 1082 | } 1083 | 1084 | //uri = urlparse.urljoin(self.get_baseuri(dc), self.path) 1085 | //uri = urllib.unquote(uri) 1086 | uri := r.RequestURI 1087 | qlog.Info("do_LOCK: uri = %s", uri) 1088 | 1089 | ifheader := r.Header.Get("If") 1090 | alreadylocked := s.isLocked(uri, ifheader) 1091 | qlog.Info("do_LOCK: alreadylocked = %s", alreadylocked) 1092 | 1093 | if body != "" && alreadylocked { 1094 | //# Full LOCK request but resource already locked 1095 | //self.responses[423] = ('Locked', 'Already locked') 1096 | w.WriteHeader(423) 1097 | return 1098 | } else if body != "" && ifheader == "" { 1099 | //# LOCK with XML information 1100 | //fmt.Println("body:", body) 1101 | lock, err := ParseLockString(body) 1102 | //data, err := s._lock_unlock_parse(body) 1103 | if err != nil { 1104 | fmt.Println(err) 1105 | w.WriteHeader(500) 1106 | return 1107 | } 1108 | lock.timeout = ParseTimeOut(r) 1109 | lock.uri = r.RequestURI 1110 | lock.token = generateToken() 1111 | //fmt.Println("lock:", data) 1112 | token, result := s._lock_unlock_create(lock, depth) 1113 | 1114 | if result != "" { 1115 | w.Write([]byte(result)) 1116 | w.Header().Set("Content-Type", "text/xml; charset=utf-8") 1117 | w.WriteHeader(207) 1118 | } else { 1119 | //lock := s.getLock(token) 1120 | w.Header().Set("Lock-Token", fmt.Sprintf("", token)) 1121 | w.Header().Set("Content-Type", "text/xml; charset=utf-8") 1122 | output := lock.asXML("D:", false) 1123 | fmt.Println("output:", output) 1124 | w.Write([]byte(output)) 1125 | //w.WriteHeader(200) 1126 | } 1127 | } else { 1128 | //d# refresh request - refresh lock timeout 1129 | taglist := IfParser(ifheader) 1130 | var found bool 1131 | for _, tag := range taglist { 1132 | for _, listitem := range tag.list { 1133 | token := tokenFinder(listitem) 1134 | if token != "" && s.hasLock(token) { 1135 | lock := s.getLock(token) 1136 | timeout := "Infinite" 1137 | if r.Header.Get("Timeout") != "" { 1138 | timeout = r.Header.Get("Timeout") 1139 | } 1140 | to, _ := strconv.Atoi(timeout) 1141 | lock.SetTimeout(time.Duration(to)) //# automatically refreshes 1142 | found = true 1143 | 1144 | w.WriteHeader(200) 1145 | w.Write([]byte(lock.asXML("", true))) 1146 | w.Header().Set("Content-Type", "text/xml; encoding=utf-8") 1147 | break 1148 | } 1149 | } 1150 | if found { 1151 | break 1152 | } 1153 | } 1154 | //# we didn't find any of the tokens mentioned - means 1155 | //# that table was cleared or another error 1156 | if !found { 1157 | w.WriteHeader(412) //a# precondition failed 1158 | } 1159 | } 1160 | } 1161 | 1162 | // takes a string like ' and returns the token 1163 | // part. 1164 | func tokenFinder(token string) string { 1165 | if token == "" { 1166 | return "" 1167 | } 1168 | if token[0] == '[' { 1169 | return "" 1170 | } 1171 | if token[0] == '<' { 1172 | token = token[1 : len(token)-1] 1173 | } 1174 | return token[strings.Index(token, ":")+1:] 1175 | } 1176 | 1177 | func (s *Server) doUnlock(w http.ResponseWriter, r *http.Request) { 1178 | /*if s.ReadOnly { 1179 | w.WriteHeader(StatusForbidden) 1180 | return 1181 | } 1182 | 1183 | if s.isLockedRequest(r) { 1184 | w.WriteHeader(StatusLocked) 1185 | return 1186 | } 1187 | 1188 | // TODO: unlock 1189 | w.WriteHeader(StatusNotImplemented) 1190 | return*/ 1191 | 1192 | //dc = self.IFACE_CLASS 1193 | 1194 | //if self._config.DAV.getboolean('verbose') is True: 1195 | qlog.Info("UNLOCKing resource", r.Header) 1196 | 1197 | //uri := urlparse.urljoin(self.get_baseuri(dc), self.path) 1198 | //uri = urllib.unquote(uri) 1199 | uri := r.RequestURI 1200 | 1201 | // check lock token - must contain a dash 1202 | lockToken := r.Header.Get("Lock-Token") 1203 | if !strings.Contains(lockToken, "-") { 1204 | w.WriteHeader(400) 1205 | return 1206 | } 1207 | 1208 | ifHeader := r.Header.Get("If") 1209 | token := tokenFinder(lockToken) 1210 | if s.isLocked(uri, ifHeader) { 1211 | s.delLock(token) 1212 | } 1213 | 1214 | w.WriteHeader(204) 1215 | //self.send_body(None, '204', 'Ok', 'Ok') 1216 | } 1217 | 1218 | func (s *Server) doOptions(w http.ResponseWriter, r *http.Request) { 1219 | // http://www.webdav.org/specs/rfc4918.html#dav.compliance.classes 1220 | w.Header().Set("DAV", "1, 2") 1221 | 1222 | w.Header().Set("Allow", s.methodsAllowed(s.url2path(r.URL))) 1223 | w.Header().Set("MS-Author-Via", "DAV") 1224 | } 1225 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package webdav 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net/http" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type Depth string 14 | 15 | var ( 16 | NoDepth = Depth("") 17 | Depth0 = Depth("0") 18 | Depth1 = Depth("1") 19 | DepthInfinity = Depth("infinity") 20 | ) 21 | 22 | func ParseDepth(r *http.Request) Depth { 23 | return Depth(r.Header.Get("Depth")) 24 | } 25 | 26 | type LockToken string 27 | 28 | var ( 29 | NoLockToken = LockToken("") 30 | ) 31 | 32 | func GenToken() LockToken { 33 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 34 | return LockToken(fmt.Sprintf("%s-%s-00105A989226:%.03f", 35 | r.Int31(), r.Int31(), time.Now().UnixNano())) 36 | } 37 | 38 | func ParseToken(r *http.Request) LockToken { 39 | return "" 40 | } 41 | 42 | var IfHdr = regexp.MustCompile(`(?P<.+?>)?\s*\((?P[^)]+)\)`) 43 | 44 | var ListItem = regexp.MustCompile( 45 | `(?Pnot)?\s*(?P<[a-zA-Z]+:[^>]*>|\[.*?\])`) 46 | 47 | type TagList struct { 48 | resource string 49 | list []string 50 | NOTTED int 51 | } 52 | 53 | func IfParser(hdr string) []*TagList { 54 | out := make([]*TagList, 0) 55 | /*i := 0 56 | for { 57 | m := IfHdr.FindString(hdr[i:]) 58 | if m == ""{ 59 | break 60 | } 61 | 62 | i = i + m.end() 63 | tag := new(TagList) 64 | tag.resource = m.group("resource") 65 | // We need to delete < > 66 | if tag.resource != "" { 67 | tag.resource = tag.resource[1:-1] 68 | } 69 | listitem = m.group("listitem") 70 | tag.NOTTED, tag.list = ListParser(listitem) 71 | append(out, tag) 72 | }*/ 73 | 74 | return out 75 | } 76 | 77 | const ( 78 | infinite = "Infinite" 79 | ) 80 | 81 | type TimeOut time.Duration 82 | 83 | func (to TimeOut) String() string { 84 | fmt.Println("===", time.Duration(to), "===") 85 | if int64(to) == 0 { 86 | return infinite 87 | } 88 | return fmt.Sprintf("Second-%d", time.Duration(to)/time.Second) 89 | } 90 | 91 | // Infinite, Second-4100000000 92 | func ParseTimeOut(req *http.Request) TimeOut { 93 | tm := req.Header.Get("Timeout") 94 | if tm == "" || tm == infinite { 95 | return TimeOut(0) 96 | } 97 | 98 | ss := strings.Split(tm, "-") 99 | if len(ss) != 2 { 100 | return TimeOut(0) 101 | } 102 | 103 | a, err := strconv.Atoi(ss[1]) 104 | if err != nil { 105 | return TimeOut(0) 106 | } 107 | return TimeOut(time.Duration(a) * time.Second) 108 | } 109 | 110 | func IsOverwrite(req *http.Request) bool { 111 | ow := req.Header.Get("Overwrite") 112 | return ow == "T" || ow == "" 113 | } 114 | --------------------------------------------------------------------------------