|
|
|
|
|
|
|
|
|
package webdav |
|
|
|
|
|
|
|
|
|
import ( |
|
"bytes" |
|
"encoding/xml" |
|
"fmt" |
|
"io" |
|
"net/http" |
|
"time" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ixml "github.com/alist-org/alist/v3/server/webdav/internal/xml" |
|
) |
|
|
|
|
|
type lockInfo struct { |
|
XMLName ixml.Name `xml:"lockinfo"` |
|
Exclusive *struct{} `xml:"lockscope>exclusive"` |
|
Shared *struct{} `xml:"lockscope>shared"` |
|
Write *struct{} `xml:"locktype>write"` |
|
Owner owner `xml:"owner"` |
|
} |
|
|
|
|
|
type owner struct { |
|
InnerXML string `xml:",innerxml"` |
|
} |
|
|
|
func readLockInfo(r io.Reader) (li lockInfo, status int, err error) { |
|
c := &countingReader{r: r} |
|
if err = ixml.NewDecoder(c).Decode(&li); err != nil { |
|
if err == io.EOF { |
|
if c.n == 0 { |
|
|
|
|
|
return lockInfo{}, 0, nil |
|
} |
|
err = errInvalidLockInfo |
|
} |
|
return lockInfo{}, http.StatusBadRequest, err |
|
} |
|
|
|
|
|
if li.Exclusive == nil || li.Shared != nil || li.Write == nil { |
|
return lockInfo{}, http.StatusNotImplemented, errUnsupportedLockInfo |
|
} |
|
return li, 0, nil |
|
} |
|
|
|
type countingReader struct { |
|
n int |
|
r io.Reader |
|
} |
|
|
|
func (c *countingReader) Read(p []byte) (int, error) { |
|
n, err := c.r.Read(p) |
|
c.n += n |
|
return n, err |
|
} |
|
|
|
func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) { |
|
depth := "infinity" |
|
if ld.ZeroDepth { |
|
depth = "0" |
|
} |
|
timeout := ld.Duration / time.Second |
|
return fmt.Fprintf(w, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"+ |
|
"<D:prop xmlns:D=\"DAV:\"><D:lockdiscovery><D:activelock>\n"+ |
|
" <D:locktype><D:write/></D:locktype>\n"+ |
|
" <D:lockscope><D:exclusive/></D:lockscope>\n"+ |
|
" <D:depth>%s</D:depth>\n"+ |
|
" <D:owner>%s</D:owner>\n"+ |
|
" <D:timeout>Second-%d</D:timeout>\n"+ |
|
" <D:locktoken><D:href>%s</D:href></D:locktoken>\n"+ |
|
" <D:lockroot><D:href>%s</D:href></D:lockroot>\n"+ |
|
"</D:activelock></D:lockdiscovery></D:prop>", |
|
depth, ld.OwnerXML, timeout, escape(token), escape(ld.Root), |
|
) |
|
} |
|
|
|
func escape(s string) string { |
|
for i := 0; i < len(s); i++ { |
|
switch s[i] { |
|
case '"', '&', '\'', '<', '>': |
|
b := bytes.NewBuffer(nil) |
|
ixml.EscapeText(b, []byte(s)) |
|
return b.String() |
|
} |
|
} |
|
return s |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
func next(d *ixml.Decoder) (ixml.Token, error) { |
|
for { |
|
t, err := d.Token() |
|
if err != nil { |
|
return t, err |
|
} |
|
switch t.(type) { |
|
case ixml.Comment, ixml.Directive, ixml.ProcInst: |
|
continue |
|
default: |
|
return t, nil |
|
} |
|
} |
|
} |
|
|
|
|
|
type propfindProps []xml.Name |
|
|
|
|
|
|
|
|
|
|
|
func (pn *propfindProps) UnmarshalXML(d *ixml.Decoder, start ixml.StartElement) error { |
|
for { |
|
t, err := next(d) |
|
if err != nil { |
|
return err |
|
} |
|
switch t.(type) { |
|
case ixml.EndElement: |
|
if len(*pn) == 0 { |
|
return fmt.Errorf("%s must not be empty", start.Name.Local) |
|
} |
|
return nil |
|
case ixml.StartElement: |
|
name := t.(ixml.StartElement).Name |
|
t, err = next(d) |
|
if err != nil { |
|
return err |
|
} |
|
if _, ok := t.(ixml.EndElement); !ok { |
|
return fmt.Errorf("unexpected token %T", t) |
|
} |
|
*pn = append(*pn, xml.Name(name)) |
|
} |
|
} |
|
} |
|
|
|
|
|
type propfind struct { |
|
XMLName ixml.Name `xml:"DAV: propfind"` |
|
Allprop *struct{} `xml:"DAV: allprop"` |
|
Propname *struct{} `xml:"DAV: propname"` |
|
Prop propfindProps `xml:"DAV: prop"` |
|
Include propfindProps `xml:"DAV: include"` |
|
} |
|
|
|
func readPropfind(r io.Reader) (pf propfind, status int, err error) { |
|
c := countingReader{r: r} |
|
if err = ixml.NewDecoder(&c).Decode(&pf); err != nil { |
|
if err == io.EOF { |
|
if c.n == 0 { |
|
|
|
|
|
return propfind{Allprop: new(struct{})}, 0, nil |
|
} |
|
err = errInvalidPropfind |
|
} |
|
return propfind{}, http.StatusBadRequest, err |
|
} |
|
|
|
if pf.Allprop == nil && pf.Include != nil { |
|
return propfind{}, http.StatusBadRequest, errInvalidPropfind |
|
} |
|
if pf.Allprop != nil && (pf.Prop != nil || pf.Propname != nil) { |
|
return propfind{}, http.StatusBadRequest, errInvalidPropfind |
|
} |
|
if pf.Prop != nil && pf.Propname != nil { |
|
return propfind{}, http.StatusBadRequest, errInvalidPropfind |
|
} |
|
if pf.Propname == nil && pf.Allprop == nil && pf.Prop == nil { |
|
return propfind{}, http.StatusBadRequest, errInvalidPropfind |
|
} |
|
return pf, 0, nil |
|
} |
|
|
|
|
|
|
|
type Property struct { |
|
|
|
XMLName xml.Name |
|
|
|
|
|
Lang string `xml:"xml:lang,attr,omitempty"` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
InnerXML []byte `xml:",innerxml"` |
|
} |
|
|
|
|
|
|
|
type ixmlProperty struct { |
|
XMLName ixml.Name |
|
Lang string `xml:"xml:lang,attr,omitempty"` |
|
InnerXML []byte `xml:",innerxml"` |
|
} |
|
|
|
|
|
|
|
type xmlError struct { |
|
XMLName ixml.Name `xml:"D:error"` |
|
InnerXML []byte `xml:",innerxml"` |
|
} |
|
|
|
|
|
|
|
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"` |
|
} |
|
|
|
|
|
|
|
type ixmlPropstat struct { |
|
Prop []ixmlProperty `xml:"D:prop>_ignored_"` |
|
Status string `xml:"D:status"` |
|
Error *xmlError `xml:"D:error"` |
|
ResponseDescription string `xml:"D:responsedescription,omitempty"` |
|
} |
|
|
|
|
|
|
|
func (ps propstat) MarshalXML(e *ixml.Encoder, start ixml.StartElement) error { |
|
|
|
ixmlPs := ixmlPropstat{ |
|
Prop: make([]ixmlProperty, len(ps.Prop)), |
|
Status: ps.Status, |
|
Error: ps.Error, |
|
ResponseDescription: ps.ResponseDescription, |
|
} |
|
for k, prop := range ps.Prop { |
|
ixmlPs.Prop[k] = ixmlProperty{ |
|
XMLName: ixml.Name(prop.XMLName), |
|
Lang: prop.Lang, |
|
InnerXML: prop.InnerXML, |
|
} |
|
} |
|
|
|
for k, prop := range ixmlPs.Prop { |
|
if prop.XMLName.Space == "DAV:" { |
|
prop.XMLName = ixml.Name{Space: "", Local: "D:" + prop.XMLName.Local} |
|
ixmlPs.Prop[k] = prop |
|
} |
|
} |
|
|
|
type newpropstat ixmlPropstat |
|
return e.EncodeElement(newpropstat(ixmlPs), start) |
|
} |
|
|
|
|
|
|
|
type response struct { |
|
XMLName ixml.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"` |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type multistatusWriter struct { |
|
|
|
|
|
|
|
|
|
responseDescription string |
|
|
|
w http.ResponseWriter |
|
enc *ixml.Encoder |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (w *multistatusWriter) write(r *response) error { |
|
switch len(r.Href) { |
|
case 0: |
|
return errInvalidResponse |
|
case 1: |
|
if len(r.Propstat) > 0 != (r.Status == "") { |
|
return errInvalidResponse |
|
} |
|
default: |
|
if len(r.Propstat) > 0 || r.Status == "" { |
|
return errInvalidResponse |
|
} |
|
} |
|
err := w.writeHeader() |
|
if err != nil { |
|
return err |
|
} |
|
return w.enc.Encode(r) |
|
} |
|
|
|
|
|
|
|
|
|
func (w *multistatusWriter) writeHeader() error { |
|
if w.enc != nil { |
|
return nil |
|
} |
|
w.w.Header().Add("Content-Type", "text/xml; charset=utf-8") |
|
w.w.WriteHeader(StatusMulti) |
|
_, err := fmt.Fprintf(w.w, `<?xml version="1.0" encoding="UTF-8"?>`) |
|
if err != nil { |
|
return err |
|
} |
|
w.enc = ixml.NewEncoder(w.w) |
|
return w.enc.EncodeToken(ixml.StartElement{ |
|
Name: ixml.Name{ |
|
Space: "DAV:", |
|
Local: "multistatus", |
|
}, |
|
Attr: []ixml.Attr{{ |
|
Name: ixml.Name{Space: "xmlns", Local: "D"}, |
|
Value: "DAV:", |
|
}}, |
|
}) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (w *multistatusWriter) close() error { |
|
if w.enc == nil { |
|
return nil |
|
} |
|
var end []ixml.Token |
|
if w.responseDescription != "" { |
|
name := ixml.Name{Space: "DAV:", Local: "responsedescription"} |
|
end = append(end, |
|
ixml.StartElement{Name: name}, |
|
ixml.CharData(w.responseDescription), |
|
ixml.EndElement{Name: name}, |
|
) |
|
} |
|
end = append(end, ixml.EndElement{ |
|
Name: ixml.Name{Space: "DAV:", Local: "multistatus"}, |
|
}) |
|
for _, t := range end { |
|
err := w.enc.EncodeToken(t) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
return w.enc.Flush() |
|
} |
|
|
|
var xmlLangName = ixml.Name{Space: "http://www.w3.org/XML/1998/namespace", Local: "lang"} |
|
|
|
func xmlLang(s ixml.StartElement, d string) string { |
|
for _, attr := range s.Attr { |
|
if attr.Name == xmlLangName { |
|
return attr.Value |
|
} |
|
} |
|
return d |
|
} |
|
|
|
type xmlValue []byte |
|
|
|
func (v *xmlValue) UnmarshalXML(d *ixml.Decoder, start ixml.StartElement) error { |
|
|
|
|
|
|
|
|
|
var b bytes.Buffer |
|
e := ixml.NewEncoder(&b) |
|
for { |
|
t, err := next(d) |
|
if err != nil { |
|
return err |
|
} |
|
if e, ok := t.(ixml.EndElement); ok && e.Name == start.Name { |
|
break |
|
} |
|
if err = e.EncodeToken(t); err != nil { |
|
return err |
|
} |
|
} |
|
err := e.Flush() |
|
if err != nil { |
|
return err |
|
} |
|
*v = b.Bytes() |
|
return nil |
|
} |
|
|
|
|
|
type proppatchProps []Property |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (ps *proppatchProps) UnmarshalXML(d *ixml.Decoder, start ixml.StartElement) error { |
|
lang := xmlLang(start, "") |
|
for { |
|
t, err := next(d) |
|
if err != nil { |
|
return err |
|
} |
|
switch elem := t.(type) { |
|
case ixml.EndElement: |
|
if len(*ps) == 0 { |
|
return fmt.Errorf("%s must not be empty", start.Name.Local) |
|
} |
|
return nil |
|
case ixml.StartElement: |
|
p := Property{ |
|
XMLName: xml.Name(t.(ixml.StartElement).Name), |
|
Lang: xmlLang(t.(ixml.StartElement), lang), |
|
} |
|
err = d.DecodeElement(((*xmlValue)(&p.InnerXML)), &elem) |
|
if err != nil { |
|
return err |
|
} |
|
*ps = append(*ps, p) |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
type setRemove struct { |
|
XMLName ixml.Name |
|
Lang string `xml:"xml:lang,attr,omitempty"` |
|
Prop proppatchProps `xml:"DAV: prop"` |
|
} |
|
|
|
|
|
type propertyupdate struct { |
|
XMLName ixml.Name `xml:"DAV: propertyupdate"` |
|
Lang string `xml:"xml:lang,attr,omitempty"` |
|
SetRemove []setRemove `xml:",any"` |
|
} |
|
|
|
func readProppatch(r io.Reader) (patches []Proppatch, status int, err error) { |
|
var pu propertyupdate |
|
if err = ixml.NewDecoder(r).Decode(&pu); err != nil { |
|
return nil, http.StatusBadRequest, err |
|
} |
|
for _, op := range pu.SetRemove { |
|
remove := false |
|
switch op.XMLName { |
|
case ixml.Name{Space: "DAV:", Local: "set"}: |
|
|
|
case ixml.Name{Space: "DAV:", Local: "remove"}: |
|
for _, p := range op.Prop { |
|
if len(p.InnerXML) > 0 { |
|
return nil, http.StatusBadRequest, errInvalidProppatch |
|
} |
|
} |
|
remove = true |
|
default: |
|
return nil, http.StatusBadRequest, errInvalidProppatch |
|
} |
|
patches = append(patches, Proppatch{Remove: remove, Props: op.Prop}) |
|
} |
|
return patches, 0, nil |
|
} |
|
|