diff --git a/cmd/noah-server/noah-server.go b/cmd/noah-server/noah-server.go index ae3b2d8..64add1d 100644 --- a/cmd/noah-server/noah-server.go +++ b/cmd/noah-server/noah-server.go @@ -1,29 +1,96 @@ package main import ( + "io" "fmt" "net/http" + "strings" "noahcloud/webdav" + "noahcloud/handlers/nextcloud" + //"noahcloud/handlers/proxy" + "net/http/httptest" ) +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + func main() { - fs := &webdav.Handler{ - FileSystem: webdav.Dir("."), + nc_normal_handler := &nextcloud.Handler{} + //proxy_handler := &proxy.Handler{} + nc_webdav_handler := &webdav.Handler{ + Prefix: "/remote.php/webdav", + FileSystem: webdav.Dir("/data/ngo/files/"), } + webdav_handler := &webdav.Handler{ + Prefix: "/webdav", + FileSystem: webdav.Dir("/data/ngo/files/"), + } + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Println(r.Method, r.URL.Path) - username, password, ok := r.BasicAuth() - fmt.Println(username, password) - if !ok { - w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - w.WriteHeader(http.StatusUnauthorized) - return + + writer := httptest.NewRecorder() + + //proxy_handler.ServeHTTP(writer, r) + //fmt.Printf("%d - %s", writer.Code, writer.Body.String()) + + //copyHeader(w.Header(), writer.Header) + //w.WriteHeader(writer.StatusCode) + //io.Copy(w, writer.Body) + //return + + if strings.HasPrefix(r.URL.Path, "/remote.php/webdav") { + username, password, ok := r.BasicAuth() + fmt.Println(username, password) + if !ok { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + w.WriteHeader(http.StatusUnauthorized) + return + } + if username != "vscode" || password != "password" { + //http.Error(w, "WebDAV: need authorized!", http.StatusUnauthorized) + //return + } + nc_webdav_handler.ServeHTTP(writer, r) + + copyHeader(w.Header(), writer.Header()) + w.WriteHeader(writer.Code) + for k, vv := range writer.Header() { + for _, v := range vv { + fmt.Println(k, v) + } + } + fmt.Printf("%s\n", writer.Body.String()) + io.Copy(w, writer.Body) + } else if strings.HasPrefix(r.URL.Path, "/webdav") { + username, password, ok := r.BasicAuth() + fmt.Println(username, password) + if !ok { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + w.WriteHeader(http.StatusUnauthorized) + return + } + if username != "vscode" || password != "password" { + http.Error(w, "WebDAV: need authorized!", http.StatusUnauthorized) + return + } + webdav_handler.ServeHTTP(w, r) + } else if r.URL.Path == "/status.php" { + nc_normal_handler.ServeHTTP(w, r) + } else if r.URL.Path == "/ocs/v2.php/cloud/user" { + nc_normal_handler.ServeHTTP(w, r) + } else if r.URL.Path == "/index.php/login/v2" { // POST + nc_normal_handler.ServeHTTP(w, r) + } else if r.URL.Path == "/index.php/login/flow" { + nc_normal_handler.ServeHTTP(w, r) + } else { + http.Error(w, "Not Found", http.StatusNotFound) } - if username != "vscode" || password != "password" { - //http.Error(w, "WebDAV: need authorized!", http.StatusUnauthorized) - //return - } - fs.ServeHTTP(w, r) }) fmt.Println("wait requests") http.ListenAndServe(":8080", nil) diff --git a/handlers/nextcloud/nextcloud.go b/handlers/nextcloud/nextcloud.go new file mode 100644 index 0000000..2171ff2 --- /dev/null +++ b/handlers/nextcloud/nextcloud.go @@ -0,0 +1,77 @@ +package nextcloud + +import ( + "net/http" + "encoding/json" + "io/ioutil" +) + +type Handler struct { + +} + +type Status struct { + Installed bool `json:"installed"` + Maintenance bool `json:"maintenance"` + NeedsDbUpgrade bool `json:"needsDbUpgrade"` + Version string `json:"version"` + VersionString string `json:"versionstring"` + Edition string `json:"edition"` + ProductName string `json:"productname"` + ExtendedSupport bool `json:"extendedSupport"` +} + +type PollMsg struct { + Endpoint string `json:"endpoint"` + Token string `json:"token"` +} + +type TokenMsg struct { + Login string `json:"login"` + Poll PollMsg `json:"poll"` +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/status.php" { + status := Status{ + Installed: true, + Maintenance: false, + NeedsDbUpgrade: false, + Version: "21.0.1.1", + VersionString: "21.0.1", + Edition: "", + ProductName: "Nextcloud", + ExtendedSupport: false, + } + js, _ := json.Marshal(status) + w.Header().Set("Content-Type", "application/json") + w.Write(js) + + } else if r.URL.Path == "/index.php/login/v2" { // #1 POST to start login session + tokenMsg := TokenMsg{ + Login: "http://" + r.Host + "/login/v2/flow/mqNBzQ3AY1QFfOIzPIn5hZYeNZlZ0RDbT32b42cpmdkFjgGvqjSTsKh6OG7floh7z2JHKmWlZnl7kBFxNEQYcDnmYArkOFjXeBsAB5w3fbOcyJBmi5JBS1k1jYGL8V1D", + Poll: PollMsg { + Endpoint: "http://" + r.Host + "/login/v2/poll", + Token: "zbF2EOo2de8gNOOePERCHQgLp1FQHB8zcSQ5t1KQhtNH0fRqV85Nh5UGRm9ZqpHmigH52jCYuxLSVZpu4FjHq9j8va233OXEHqQnBnMGr4p3BoRcc957jvJEjynPvJcB", + }, + } + js, _ := json.Marshal(tokenMsg) + w.Header().Set("Content-Type", "application/json") + w.Write(js) + + } else if r.URL.Path == "/index.php/login/flow" { // # goto login page + var NewUrl string = "nc://login/server:http://" + r.Host + "&user:vscode&password:TEwNL0Sbkeln0TKdlcmNukkhQaOc6is0GBWlXIoO1YOByPweiSpoISuN5UI4fSGRBOgGE61I" + http.Redirect(w, r, NewUrl, http.StatusSeeOther) + + } else if r.URL.Path == "/login/flow/grant" { // #3 redirect to grant page + + } else if r.URL.Path == "/ocs/v2.php/cloud/user" { // #4 retreive user metadata + dat, _ := ioutil.ReadFile("/data/ngo/conf/ocs_v2.php_cloud_user.xml") + js := []byte(string(dat)) + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.Write(js) + + } else { + http.Error(w, "Not Found", http.StatusNotFound) + } +} \ No newline at end of file diff --git a/handlers/proxy/proxy.go b/handlers/proxy/proxy.go new file mode 100644 index 0000000..8deaf48 --- /dev/null +++ b/handlers/proxy/proxy.go @@ -0,0 +1,88 @@ +package proxy + +import ( + "net/http" + "net/http/httputil" + "io/ioutil" + "fmt" + "strings" + "bytes" + "io" +) + +type Handler struct { + +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // we need to buffer the body if we want to read it here and send it + // in the request. + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + fmt.Println("request body--start") + fmt.Println(string(body)) + fmt.Println("request body--stop") + + // you can reassign the body if you need to parse it as multipart + r.Body = ioutil.NopCloser(bytes.NewReader(body)) + + // create a new url from the raw RequestURI sent by the client + url := fmt.Sprintf("%s://%s%s", "http", r.Host, r.RequestURI) + fmt.Println("-----Request-----", r.RequestURI) + fmt.Println(r.Method, url) + + proxyReq, err := http.NewRequest(r.Method, url, bytes.NewReader(body)) + + // We may want to filter some headers, otherwise we could just use a shallow copy + // proxyReq.Header = r.Header + proxyReq.Header = make(http.Header) + for h, val := range r.Header { + tmp := []string{} + for _, v := range val{ + if h == "Accept-Encoding" && strings.Contains(v, "gzip") { + continue + } + tmp = append(tmp, v) + } + proxyReq.Header[h] = tmp + fmt.Println(h, ":", tmp) + } + + httpClient := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + resp, err := httpClient.Do(proxyReq) + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + + copyHeader(w.Header(), resp.Header) + w.WriteHeader(resp.StatusCode) + + responseData, err := httputil.DumpResponse(resp, true) + if err != nil { + fmt.Println("err", err) + } + responseString := string(responseData) + fmt.Println("-----Response-----", r.RequestURI) + fmt.Println(responseString) + + io.Copy(w, resp.Body) + + defer resp.Body.Close() + // legacy code +} \ No newline at end of file diff --git a/webdav/prop.go b/webdav/prop.go index d65232c..9fc1892 100644 --- a/webdav/prop.go +++ b/webdav/prop.go @@ -107,18 +107,23 @@ var liveProps = map[xml.Name]struct { findFn func(context.Context, FileSystem, string, os.FileInfo) (string, error) // dir is true if the property applies to directories. dir bool + // file is true if the property applies to files. + file bool }{ {Space: "DAV:", Local: "resourcetype"}: { findFn: findResourceType, dir: true, + file: true, }, {Space: "DAV:", Local: "displayname"}: { findFn: findDisplayName, dir: true, + file: true, }, {Space: "DAV:", Local: "getcontentlength"}: { findFn: findContentLength, dir: false, + file: true, }, {Space: "DAV:", Local: "getlastmodified"}: { findFn: findLastModified, @@ -130,26 +135,143 @@ var liveProps = map[xml.Name]struct { // sortable by getlastmodified date, so this value is true, not false. // See golang.org/issue/15334. dir: true, + file:true, }, {Space: "DAV:", Local: "creationdate"}: { findFn: nil, dir: false, + file: true, }, {Space: "DAV:", Local: "getcontentlanguage"}: { findFn: nil, dir: false, + file: true, }, {Space: "DAV:", Local: "getcontenttype"}: { findFn: findContentType, dir: false, + file: true, }, {Space: "DAV:", Local: "getetag"}: { findFn: findETag, - // findETag implements ETag as the concatenated hex values of a file's - // modification time and size. This is not a reliable synchronization - // mechanism for directories, so we do not advertise getetag for DAV - // collections. + dir: true, + file: true, + }, + {Space: "DAV:", Local: "quota-available-bytes"}: { + findFn: fnGetQuotaAvailableBytes, + dir: true, + file: false, + }, + {Space: "DAV:", Local: "quota-used-bytes"}: { + findFn: fnGetQuotaUsedBytes, + dir: true, + file: false, + }, + + {Space: "http://owncloud.org/ns", Local: "permissions"}: { + findFn: fnGetPermissions, + dir: true, + file: true, + }, + {Space: "http://owncloud.org/ns", Local: "id"}: { + findFn: fnGetID, + dir: true, + file: true, + }, + {Space: "http://owncloud.org/ns", Local: "fileid"}: { + findFn: fnGetFileID, + dir: true, + file: true, + }, + {Space: "http://owncloud.org/ns", Local: "size"}: { + findFn: fnGetSize, + dir: true, + file: true, + }, + {Space: "http://owncloud.org/ns", Local: "favorite"}: { + findFn: fnGetFavorite, + dir: true, + file: true, + }, + {Space: "http://owncloud.org/ns", Local: "share-types"}: { + findFn: fnGetShareTypes, + dir: true, + file: true, + }, + {Space: "http://owncloud.org/ns", Local: "owner-id"}: { + findFn: fnGetOwnerID, + dir: true, + file: true, + }, + {Space: "http://owncloud.org/ns", Local: "owner-display-name"}: { + findFn: fnGetOwnerDisplayName, + dir: true, + file: true, + }, + {Space: "http://owncloud.org/ns", Local: "comments-unread"}: { + findFn: fnGetCommentsUnread, + dir: true, + file: true, + }, + {Space: "http://owncloud.org/ns", Local: "checksums"}: { + findFn: fnGetChecksums, dir: false, + file: false, + }, + {Space: "http://owncloud.org/ns", Local: "downloadURL"}: { + findFn: fnGetDownloadURL, + dir: false, + file: true, + }, + {Space: "http://owncloud.org/ns", Local: "data-fingerprint"}: { + findFn: fnGetDataFingerprint, + dir: true, + file: true, + }, + {Space: "http://nextcloud.org/ns", Local: "creation_time"}: { + findFn: fnGetCreationTime, + dir: false, + file: true, + }, + {Space: "http://nextcloud.org/ns", Local: "upload_time"}: { + findFn: fnGetUploadTime, + dir: false, + file: true, + }, + {Space: "http://nextcloud.org/ns", Local: "is-encrypted"}: { + findFn: fnGetIsEncrypted, + dir: true, + file: false, + }, + {Space: "http://nextcloud.org/ns", Local: "has-preview"}: { + findFn: fnGetHasPreview, + dir: true, + file: true, + }, + {Space: "http://nextcloud.org/ns", Local: "mount-type"}: { + findFn: fnGetMountType, + dir: true, + file: true, + }, + {Space: "http://nextcloud.org/ns", Local: "rich-workspace"}: { + findFn: fnGetRichWorkspace, + dir: false, + file: false, + }, + {Space: "http://nextcloud.org/ns", Local: "note"}: { + findFn: fnGetNote, + dir: true, + file: true, + }, + {Space: "http://open-collaboration-services.org/ns", Local: "share-permissions"}: { + findFn: fnGetSharePermissionsOCS, + dir: true, + file: true, + }, + {Space: "http://open-cloud-mesh.org/ns", Local: "share-permissions"}: { + findFn: fnGetSharePermissionsOCM, + dir: true, + file: true, }, } @@ -188,7 +310,8 @@ func props(ctx context.Context, fs FileSystem, name string, pnames []xml.Name) ( continue } // Otherwise, it must either be a live property or we don't know it. - if prop := liveProps[pn]; prop.findFn != nil && (prop.dir || !isDir) { + //fmt.Println(liveProps[pn], liveProps[pn].findFn, name, pn) + if prop := liveProps[pn]; prop.findFn != nil && ((prop.dir && isDir) || (prop.file && !isDir)){ innerXML, err := prop.findFn(ctx, fs, name, fi) if err != nil { return nil, err @@ -287,7 +410,7 @@ func escapeXML(s string) string { func findResourceType(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { if fi.IsDir() { - return ``, nil + return ``, nil } return "", nil } @@ -358,33 +481,104 @@ func findContentType(ctx context.Context, fs FileSystem, name string, fi os.File return ctype, err } -// ETager is an optional interface for the os.FileInfo objects -// returned by the FileSystem. -// -// If this interface is defined then it will be used to read the ETag -// for the object. -// -// If this interface is not defined an ETag will be computed using the -// ModTime() and the Size() methods of the os.FileInfo object. -type ETager interface { - // ETag returns an ETag for the file. This should be of the - // form "value" or W/"value" - // - // If this returns error ErrNotImplemented then the error will - // be ignored and the base implementation will be used - // instead. - ETag(ctx context.Context) (string, error) +func findETag(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()), nil } -func findETag(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { - if do, ok := fi.(ETager); ok { - etag, err := do.ETag(ctx) - if err != ErrNotImplemented { - return etag, err - } +func fnGetQuotaAvailableBytes(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "1057129933", nil +} + +func fnGetQuotaUsedBytes(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "16611891", nil +} + +func fnGetPermissions(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + if fi.IsDir() { + return "RGDNVCK", nil } - // The Apache http 2.4 web server by default concatenates the - // modification time and size of a file. We replicate the heuristic - // with nanosecond granularity. - return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size()), nil + return "RGDNVW", nil +} + +func fnGetID(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return name, nil +} + +func fnGetFileID(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "0", nil +} + +func fnGetSize(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "16611891", nil +} + +func fnGetFavorite(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "0", nil +} + +func fnGetShareTypes(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "", nil +} + +func fnGetOwnerID(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "vscode", nil +} + +func fnGetOwnerDisplayName(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "vscode", nil +} + +func fnGetCommentsUnread(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "0", nil +} + +func fnGetChecksums(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "", nil +} + +func fnGetDownloadURL(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "", nil +} + +func fnGetDataFingerprint(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "", nil +} + +func fnGetCreationTime(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "0", nil +} + +func fnGetUploadTime(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "0", nil +} + +func fnGetIsEncrypted(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "0", nil +} + +func fnGetHasPreview(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "false", nil +} + +func fnGetMountType(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "", nil +} + +func fnGetRichWorkspace(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "", nil +} + +func fnGetNote(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "", nil +} + +func fnGetSharePermissionsOCS(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + if fi.IsDir() { + return "31", nil + } + return "19", nil +} + +func fnGetSharePermissionsOCM(ctx context.Context, fs FileSystem, name string, fi os.FileInfo) (string, error) { + return "[\"share\",\"read\",\"write\"]", nil } \ No newline at end of file diff --git a/webdav/webdav.go b/webdav/webdav.go index 4ceca99..a69da6b 100644 --- a/webdav/webdav.go +++ b/webdav/webdav.go @@ -173,7 +173,19 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, if err != nil { return http.StatusInternalServerError, err } - w.Header().Set("ETag", etag) + + w.Header().Set("Etag", etag) + w.Header().Set("Oc-Etag", etag) + w.Header().Set("Oc-Fileid", etag) + w.Header().Set("X-Hash-Md5", "97a81b5ce26cfa53e66c21da5c3d60ee") + w.Header().Set("X-Hash-Sha1", "0bdaebd3d8915d4a7de79cdc76cc42ad757a073d") + w.Header().Set("X-Hash-Sha256", "7de442457c55ce163ca64e7952cf4e1c796488f24f2adae8140f4f3160ea94c2") + if r.Header.Get("X-Oc-Ctime") != "" { + w.Header().Set("X-Oc-Ctime", "accepted") + } + if r.Header.Get("X-Oc-Mtime") != "" { + w.Header().Set("X-Oc-Mtime", "accepted") + } return http.StatusCreated, nil } @@ -278,7 +290,6 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status if err != nil { return status, err } - mw := multistatusWriter{w: w} walkFn := func(reqPath string, info os.FileInfo, err error) error { diff --git a/webdav/xml.go b/webdav/xml.go index b0d704e..a52db5f 100644 --- a/webdav/xml.go +++ b/webdav/xml.go @@ -158,31 +158,31 @@ type xmlProperty struct { } // http://www.webdav.org/specs/rfc4918.html#ELEMENT_error -// See multistatusWriter for the "D:" namespace prefix. +// See multistatusWriter for the "d:" namespace prefix. type xmlError struct { - XMLName xml.Name `xml:"D:error"` + XMLName xml.Name `xml:"d:error"` InnerXML []byte `xml:",innerxml"` } // http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat -// See multistatusWriter for the "D:" namespace prefix. +// See multistatusWriter for the "d:" namespace prefix. type propstat struct { - Prop []Property `xml:"D:prop>_ignored_"` - Status string `xml:"D:status"` - Error *xmlError `xml:"D:error"` - ResponseDescription string `xml:"D:responsedescription,omitempty"` + Prop []Property `xml:"d:prop>_ignored_"` + Status string `xml:"d:status"` + Error *xmlError `xml:"d:error"` + ResponseDescription string `xml:"d:responsedescription,omitempty"` } // xmlPropstat is the same as the propstat type except it holds an xml.Name // instead of an xml.Name. type xmlPropstat struct { - Prop []xmlProperty `xml:"D:prop>_ignored_"` - Status string `xml:"D:status"` - Error *xmlError `xml:"D:error"` - ResponseDescription string `xml:"D:responsedescription,omitempty"` + Prop []xmlProperty `xml:"d:prop>_ignored_"` + Status string `xml:"d:status"` + Error *xmlError `xml:"d:error"` + ResponseDescription string `xml:"d:responsedescription,omitempty"` } -// MarshalXML prepends the "D:" namespace prefix on properties in the DAV: namespace +// MarshalXML prepends the "d:" namespace prefix on properties in the DAV: namespace // before encoding. See multistatusWriter. func (ps propstat) MarshalXML(e *xml.Encoder, start xml.StartElement) error { // Convert from a propstat to an xmlPropstat. @@ -202,7 +202,19 @@ func (ps propstat) MarshalXML(e *xml.Encoder, start xml.StartElement) error { for k, prop := range xmlPs.Prop { if prop.XMLName.Space == "DAV:" { - prop.XMLName = xml.Name{Space: "", Local: "D:" + prop.XMLName.Local} + prop.XMLName = xml.Name{Space: "", Local: "d:" + prop.XMLName.Local} + xmlPs.Prop[k] = prop + } else if prop.XMLName.Space == "http://owncloud.org/ns" { + prop.XMLName = xml.Name{Space: "", Local: "oc:" + prop.XMLName.Local} + xmlPs.Prop[k] = prop + } else if prop.XMLName.Space == "http://nextcloud.org/ns" { + prop.XMLName = xml.Name{Space: "", Local: "nc:" + prop.XMLName.Local} + xmlPs.Prop[k] = prop + } else if prop.XMLName.Space == "http://open-collaboration-services.org/ns" { + prop.XMLName = xml.Name{Space: "http://open-collaboration-services.org/ns", Local: "x1:" + prop.XMLName.Local} + xmlPs.Prop[k] = prop + } else if prop.XMLName.Space == "http://open-cloud-mesh.org/ns" { + prop.XMLName = xml.Name{Space: "http://open-cloud-mesh.org/ns", Local: "x2:" + prop.XMLName.Local} xmlPs.Prop[k] = prop } } @@ -212,20 +224,20 @@ func (ps propstat) MarshalXML(e *xml.Encoder, start xml.StartElement) error { } // http://www.webdav.org/specs/rfc4918.html#ELEMENT_response -// See multistatusWriter for the "D:" namespace prefix. +// See multistatusWriter for the "d:" namespace prefix. type response struct { - XMLName xml.Name `xml:"D:response"` - Href []string `xml:"D:href"` - Propstat []propstat `xml:"D:propstat"` - Status string `xml:"D:status,omitempty"` - Error *xmlError `xml:"D:error"` - ResponseDescription string `xml:"D:responsedescription,omitempty"` + XMLName xml.Name `xml:"d:response"` + Href []string `xml:"d:href"` + Propstat []propstat `xml:"d:propstat"` + Status string `xml:"d:status,omitempty"` + Error *xmlError `xml:"d:error"` + ResponseDescription string `xml:"d:responsedescription,omitempty"` } // MultistatusWriter marshals one or more Responses into a XML // multistatus response. // See http://www.webdav.org/specs/rfc4918.html#ELEMENT_multistatus -// TODO(rsto, mpl): As a workaround, the "D:" namespace prefix, defined as +// TODO(rsto, mpl): As a workaround, the "d:" namespace prefix, defined as // "DAV:" on this element, is prepended on the nested response, as well as on all // its nested elements. All property names in the DAV: namespace are prefixed as // well. This is because some versions of Mini-Redirector (on windows 7) ignore @@ -277,7 +289,8 @@ func (w *multistatusWriter) writeHeader() error { if w.enc != nil { return nil } - w.w.Header().Add("Content-Type", "text/xml; charset=utf-8") + w.w.Header().Add("Content-Type", "application/xml; charset=utf-8") + w.w.Header().Set("DAV", "1, 3, extended-mkcol") w.w.WriteHeader(http.StatusMultiStatus) _, err := fmt.Fprintf(w.w, ``) if err != nil { @@ -287,12 +300,22 @@ func (w *multistatusWriter) writeHeader() error { return w.enc.EncodeToken(xml.StartElement{ Name: xml.Name{ Space: "", - Local: "D:multistatus", + Local: "d:multistatus", }, Attr: []xml.Attr{{ - Name: xml.Name{Space: "", Local: "xmlns:D"}, + Name: xml.Name{Space: "", Local: "xmlns:d"}, Value: "DAV:", - }}, + },{ + Name: xml.Name{Space: "", Local: "xmlns:s"}, + Value: "http://sabredav.org/ns", + },{ + Name: xml.Name{Space: "", Local: "xmlns:oc"}, + Value: "http://owncloud.org/ns", + },{ + Name: xml.Name{Space: "", Local: "xmlns:nc"}, + Value: "http://nextcloud.org/ns", + }, + }, }) } @@ -314,7 +337,7 @@ func (w *multistatusWriter) close() error { ) } end = append(end, xml.EndElement{ - Name: xml.Name{Space: "", Local: "D:multistatus"}, + Name: xml.Name{Space: "", Local: "d:multistatus"}, }) for _, t := range end { err := w.enc.EncodeToken(t)