diff --git a/search/cmd/search/main.go b/search/cmd/search/main.go new file mode 100644 index 0000000000..71f1420618 --- /dev/null +++ b/search/cmd/search/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + + "github.com/owncloud/ocis/search/pkg/command" + "github.com/owncloud/ocis/search/pkg/config/defaults" +) + +func main() { + if err := command.Execute(defaults.DefaultConfig()); err != nil { + os.Exit(1) + } +} diff --git a/webdav/pkg/errors/error.go b/webdav/pkg/errors/error.go new file mode 100644 index 0000000000..ba7f9b908a --- /dev/null +++ b/webdav/pkg/errors/error.go @@ -0,0 +1,170 @@ +package errors + +import ( + "bytes" + "encoding/xml" + "net/http" + + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/cs3org/reva/v2/pkg/rgrpc/status" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +var sabreException = map[int]string{ + + // the commented states have no corresponding exception in sabre/dav, + // see https://github.com/sabre-io/dav/tree/master/lib/DAV/Exception + + // http.StatusMultipleChoices: "Multiple Choices", + // http.StatusMovedPermanently: "Moved Permanently", + // http.StatusFound: "Found", + // http.StatusSeeOther: "See Other", + // http.StatusNotModified: "Not Modified", + // http.StatusUseProxy: "Use Proxy", + // http.StatusTemporaryRedirect: "Temporary Redirect", + // http.StatusPermanentRedirect: "Permanent Redirect", + + http.StatusBadRequest: "Sabre\\DAV\\Exception\\BadRequest", + http.StatusUnauthorized: "Sabre\\DAV\\Exception\\NotAuthenticated", + http.StatusPaymentRequired: "Sabre\\DAV\\Exception\\PaymentRequired", + http.StatusForbidden: "Sabre\\DAV\\Exception\\Forbidden", // InvalidResourceType, InvalidSyncToken, TooManyMatches + http.StatusNotFound: "Sabre\\DAV\\Exception\\NotFound", + http.StatusMethodNotAllowed: "Sabre\\DAV\\Exception\\MethodNotAllowed", + // http.StatusNotAcceptable: "Not Acceptable", + // http.StatusProxyAuthRequired: "Proxy Authentication Required", + // http.StatusRequestTimeout: "Request Timeout", + http.StatusConflict: "Sabre\\DAV\\Exception\\Conflict", // LockTokenMatchesRequestUri + // http.StatusGone: "Gone", + http.StatusLengthRequired: "Sabre\\DAV\\Exception\\LengthRequired", + http.StatusPreconditionFailed: "Sabre\\DAV\\Exception\\PreconditionFailed", + // http.StatusRequestEntityTooLarge: "Request Entity Too Large", + // http.StatusRequestURITooLong: "Request URI Too Long", + http.StatusUnsupportedMediaType: "Sabre\\DAV\\Exception\\UnsupportedMediaType", // ReportNotSupported + http.StatusRequestedRangeNotSatisfiable: "Sabre\\DAV\\Exception\\RequestedRangeNotSatisfiable", + // http.StatusExpectationFailed: "Expectation Failed", + // http.StatusTeapot: "I'm a teapot", + // http.StatusMisdirectedRequest: "Misdirected Request", + // http.StatusUnprocessableEntity: "Unprocessable Entity", + http.StatusLocked: "Sabre\\DAV\\Exception\\Locked", // ConflictingLock + // http.StatusFailedDependency: "Failed Dependency", + // http.StatusTooEarly: "Too Early", + // http.StatusUpgradeRequired: "Upgrade Required", + // http.StatusPreconditionRequired: "Precondition Required", + // http.StatusTooManyRequests: "Too Many Requests", + // http.StatusRequestHeaderFieldsTooLarge: "Request Header Fields Too Large", + // http.StatusUnavailableForLegalReasons: "Unavailable For Legal Reasons", + + // http.StatusInternalServerError: "Internal Server Error", + http.StatusNotImplemented: "Sabre\\DAV\\Exception\\NotImplemented", + // http.StatusBadGateway: "Bad Gateway", + http.StatusServiceUnavailable: "Sabre\\DAV\\Exception\\ServiceUnavailable", + // http.StatusGatewayTimeout: "Gateway Timeout", + // http.StatusHTTPVersionNotSupported: "HTTP Version Not Supported", + // http.StatusVariantAlsoNegotiates: "Variant Also Negotiates", + http.StatusInsufficientStorage: "Sabre\\DAV\\Exception\\InsufficientStorage", + // http.StatusLoopDetected: "Loop Detected", + // http.StatusNotExtended: "Not Extended", + // http.StatusNetworkAuthenticationRequired: "Network Authentication Required", +} + +// SabreException returns a sabre exception text for the HTTP status code. It returns the empty +// string if the code is unknown. +func SabreException(code int) string { + return sabreException[code] +} + +// Exception represents a ocdav exception +type Exception struct { + Code int + Message string + Header string +} + +// Marshal just calls the xml marshaller for a given exception. +func Marshal(code int, message string, header string) ([]byte, error) { + xmlstring, err := xml.Marshal(&ErrorXML{ + Xmlnsd: "DAV", + Xmlnss: "http://sabredav.org/ns", + Exception: sabreException[code], + Message: message, + Header: header, + }) + if err != nil { + return nil, err + } + var buf bytes.Buffer + buf.WriteString(xml.Header) + buf.Write(xmlstring) + return buf.Bytes(), err +} + +// ErrorXML holds the xml representation of an error +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error +type ErrorXML struct { + XMLName xml.Name `xml:"d:error"` + Xmlnsd string `xml:"xmlns:d,attr"` + Xmlnss string `xml:"xmlns:s,attr"` + Exception string `xml:"s:exception"` + Message string `xml:"s:message"` + InnerXML []byte `xml:",innerxml"` + // Header is used to indicate the conflicting request header + Header string `xml:"s:header,omitempty"` +} + +var ( + // ErrInvalidDepth is an invalid depth header error + ErrInvalidDepth = errors.New("webdav: invalid depth") + // ErrInvalidPropfind is an invalid propfind error + ErrInvalidPropfind = errors.New("webdav: invalid propfind") + // ErrInvalidProppatch is an invalid proppatch error + ErrInvalidProppatch = errors.New("webdav: invalid proppatch") + // ErrInvalidLockInfo is an invalid lock error + ErrInvalidLockInfo = errors.New("webdav: invalid lock info") + // ErrUnsupportedLockInfo is an unsupported lock error + ErrUnsupportedLockInfo = errors.New("webdav: unsupported lock info") + // ErrInvalidTimeout is an invalid timeout error + ErrInvalidTimeout = errors.New("webdav: invalid timeout") + // ErrInvalidIfHeader is an invalid if header error + ErrInvalidIfHeader = errors.New("webdav: invalid If header") + // ErrUnsupportedMethod is an unsupported method error + ErrUnsupportedMethod = errors.New("webdav: unsupported method") + // ErrInvalidLockToken is an invalid lock token error + ErrInvalidLockToken = errors.New("webdav: invalid lock token") + // ErrConfirmationFailed is returned by a LockSystem's Confirm method. + ErrConfirmationFailed = errors.New("webdav: confirmation failed") + // ErrForbidden is returned by a LockSystem's Unlock method. + ErrForbidden = errors.New("webdav: forbidden") + // ErrLocked is returned by a LockSystem's Create, Refresh and Unlock methods. + ErrLocked = errors.New("webdav: locked") + // ErrNoSuchLock is returned by a LockSystem's Refresh and Unlock methods. + ErrNoSuchLock = errors.New("webdav: no such lock") + // ErrNotImplemented is returned when hitting not implemented code paths + ErrNotImplemented = errors.New("webdav: not implemented") +) + +// HandleErrorStatus checks the status code, logs a Debug or Error level message +// and writes an appropriate http status +func HandleErrorStatus(log *zerolog.Logger, w http.ResponseWriter, s *rpc.Status) { + hsc := status.HTTPStatusFromCode(s.Code) + if hsc == http.StatusInternalServerError { + log.Error().Interface("status", s).Int("code", hsc).Msg(http.StatusText(hsc)) + } else { + log.Debug().Interface("status", s).Int("code", hsc).Msg(http.StatusText(hsc)) + } + w.WriteHeader(hsc) +} + +// HandleWebdavError checks the status code, logs an error and creates a webdav response body +// if needed +func HandleWebdavError(log *zerolog.Logger, w http.ResponseWriter, b []byte, err error) { + if err != nil { + log.Error().Msgf("error marshaling xml response: %s", b) + w.WriteHeader(http.StatusInternalServerError) + return + } + _, err = w.Write(b) + if err != nil { + log.Err(err).Msg("error writing response") + } +} diff --git a/webdav/pkg/net/headers.go b/webdav/pkg/net/headers.go new file mode 100644 index 0000000000..5c22d8509d --- /dev/null +++ b/webdav/pkg/net/headers.go @@ -0,0 +1,48 @@ +package net + +// Common HTTP headers. +const ( + HeaderAcceptRanges = "Accept-Ranges" + HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" + HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" + HeaderContentDisposistion = "Content-Disposition" + HeaderContentLength = "Content-Length" + HeaderContentRange = "Content-Range" + HeaderContentType = "Content-Type" + HeaderETag = "ETag" + HeaderLastModified = "Last-Modified" + HeaderLocation = "Location" + HeaderRange = "Range" + HeaderIfMatch = "If-Match" +) + +// webdav headers +const ( + HeaderDav = "DAV" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.1 + HeaderDepth = "Depth" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.2 + HeaderDestination = "Destination" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.3 + HeaderIf = "If" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.4 + HeaderLockToken = "Lock-Token" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.5 + HeaderOverwrite = "Overwrite" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.6 + HeaderTimeout = "Timeout" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.7 +) + +// Non standard HTTP headers. +const ( + HeaderOCFileID = "OC-FileId" + HeaderOCETag = "OC-ETag" + HeaderOCChecksum = "OC-Checksum" + HeaderOCPermissions = "OC-Perm" + HeaderTusResumable = "Tus-Resumable" + HeaderTusVersion = "Tus-Version" + HeaderTusExtension = "Tus-Extension" + HeaderTusChecksumAlgorithm = "Tus-Checksum-Algorithm" + HeaderTusUploadExpires = "Upload-Expires" + HeaderUploadChecksum = "Upload-Checksum" + HeaderUploadLength = "Upload-Length" + HeaderUploadMetadata = "Upload-Metadata" + HeaderUploadOffset = "Upload-Offset" + HeaderOCMtime = "X-OC-Mtime" + HeaderExpectedEntityLength = "X-Expected-Entity-Length" + HeaderLitmus = "X-Litmus" +) diff --git a/webdav/pkg/net/net.go b/webdav/pkg/net/net.go new file mode 100644 index 0000000000..6ac66b74a6 --- /dev/null +++ b/webdav/pkg/net/net.go @@ -0,0 +1,12 @@ +package net + +import ( + "net/url" +) + +// EncodePath encodes the path of a url. +// +// slashes (/) are treated as path-separators. +func EncodePath(path string) string { + return (&url.URL{Path: path}).EscapedPath() +} diff --git a/webdav/pkg/prop/prop.go b/webdav/pkg/prop/prop.go new file mode 100644 index 0000000000..b34ce19fe8 --- /dev/null +++ b/webdav/pkg/prop/prop.go @@ -0,0 +1,125 @@ +package prop + +import ( + "bytes" + "encoding/xml" +) + +// PropertyXML represents a single DAV resource property as defined in RFC 4918. +// http://www.webdav.org/specs/rfc4918.html#data.model.for.resource.properties +type PropertyXML struct { + // XMLName is the fully qualified name that identifies this property. + XMLName xml.Name + + // Lang is an optional xml:lang attribute. + Lang string `xml:"xml:lang,attr,omitempty"` + + // InnerXML contains the XML representation of the property value. + // See http://www.webdav.org/specs/rfc4918.html#property_values + // + // Property values of complex type or mixed-content must have fully + // expanded XML namespaces or be self-contained with according + // XML namespace declarations. They must not rely on any XML + // namespace declarations within the scope of the XML document, + // even including the DAV: namespace. + InnerXML []byte `xml:",innerxml"` +} + +func xmlEscaped(val string) []byte { + buf := new(bytes.Buffer) + xml.Escape(buf, []byte(val)) + return buf.Bytes() +} + +// EscapedNS returns a new PropertyXML instance while xml-escaping the value +func EscapedNS(namespace string, local string, val string) PropertyXML { + return PropertyXML{ + XMLName: xml.Name{Space: namespace, Local: local}, + Lang: "", + InnerXML: xmlEscaped(val), + } +} + +// Escaped returns a new PropertyXML instance while xml-escaping the value +// TODO properly use the space +func Escaped(key, val string) PropertyXML { + return PropertyXML{ + XMLName: xml.Name{Space: "", Local: key}, + Lang: "", + InnerXML: xmlEscaped(val), + } +} + +// NotFound returns a new PropertyXML instance with an empty value +func NotFound(key string) PropertyXML { + return PropertyXML{ + XMLName: xml.Name{Space: "", Local: key}, + Lang: "", + } +} + +// NotFoundNS returns a new PropertyXML instance with the given namespace and an empty value +func NotFoundNS(namespace, key string) PropertyXML { + return PropertyXML{ + XMLName: xml.Name{Space: namespace, Local: key}, + Lang: "", + } +} + +// Raw returns a new PropertyXML instance for the given key/value pair +// TODO properly use the space +func Raw(key, val string) PropertyXML { + return PropertyXML{ + XMLName: xml.Name{Space: "", Local: key}, + Lang: "", + InnerXML: []byte(val), + } +} + +// Next returns the next token, if any, in the XML stream of d. +// RFC 4918 requires to ignore comments, processing instructions +// and directives. +// http://www.webdav.org/specs/rfc4918.html#property_values +// http://www.webdav.org/specs/rfc4918.html#xml-extensibility +func Next(d *xml.Decoder) (xml.Token, error) { + for { + t, err := d.Token() + if err != nil { + return t, err + } + switch t.(type) { + case xml.Comment, xml.Directive, xml.ProcInst: + continue + default: + return t, nil + } + } +} + +// ActiveLock holds active lock xml data +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_activelock +// +type ActiveLock struct { + XMLName xml.Name `xml:"activelock"` + Exclusive *struct{} `xml:"lockscope>exclusive,omitempty"` + Shared *struct{} `xml:"lockscope>shared,omitempty"` + Write *struct{} `xml:"locktype>write,omitempty"` + Depth string `xml:"depth"` + Owner Owner `xml:"owner,omitempty"` + Timeout string `xml:"timeout,omitempty"` + Locktoken string `xml:"locktoken>href"` + Lockroot string `xml:"lockroot>href,omitempty"` +} + +// Owner captures the inner UML of a lock owner element http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner +type Owner struct { + InnerXML string `xml:",innerxml"` +} + +// Escape repaces ", &, ', < and > with their xml representation +func Escape(s string) string { + b := bytes.NewBuffer(nil) + _ = xml.EscapeText(b, []byte(s)) + return b.String() +} diff --git a/webdav/pkg/propfind/propfind.go b/webdav/pkg/propfind/propfind.go new file mode 100644 index 0000000000..5a2f551cb5 --- /dev/null +++ b/webdav/pkg/propfind/propfind.go @@ -0,0 +1,180 @@ +package propfind + +import ( + "encoding/xml" + "fmt" + "io" + "net/http" + + "github.com/owncloud/ocis/webdav/pkg/errors" + "github.com/owncloud/ocis/webdav/pkg/prop" +) + +const ( + _spaceTypeProject = "project" +) + +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 +} + +// Props represents properties related to a resource +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind) +type Props []xml.Name + +// XML holds the xml representation of a propfind +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propfind +type XML struct { + XMLName xml.Name `xml:"DAV: propfind"` + Allprop *struct{} `xml:"DAV: allprop"` + Propname *struct{} `xml:"DAV: propname"` + Prop Props `xml:"DAV: prop"` + Include Props `xml:"DAV: include"` +} + +// PropstatXML holds the xml representation of a propfind response +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat +type PropstatXML struct { + // Prop requires DAV: to be the default namespace in the enclosing + // XML. This is due to the standard encoding/xml package currently + // not honoring namespace declarations inside a xmltag with a + // parent element for anonymous slice elements. + // Use of multistatusWriter takes care of this. + Prop []prop.PropertyXML `xml:"d:prop>_ignored_"` + Status string `xml:"d:status"` + Error *errors.ErrorXML `xml:"d:error"` + ResponseDescription string `xml:"d:responsedescription,omitempty"` +} + +// ResponseXML holds the xml representation of a propfind response +type ResponseXML struct { + XMLName xml.Name `xml:"d:response"` + Href string `xml:"d:href"` + Propstat []PropstatXML `xml:"d:propstat"` + Status string `xml:"d:status,omitempty"` + Error *errors.ErrorXML `xml:"d:error"` + ResponseDescription string `xml:"d:responsedescription,omitempty"` +} + +// MultiStatusResponseXML holds the xml representation of a multistatus propfind response +type MultiStatusResponseXML struct { + XMLName xml.Name `xml:"d:multistatus"` + XmlnsS string `xml:"xmlns:s,attr,omitempty"` + XmlnsD string `xml:"xmlns:d,attr,omitempty"` + XmlnsOC string `xml:"xmlns:oc,attr,omitempty"` + + Responses []*ResponseXML `xml:"d:response"` +} + +// ResponseUnmarshalXML is a workaround for https://github.com/golang/go/issues/13400 +type ResponseUnmarshalXML struct { + XMLName xml.Name `xml:"response"` + Href string `xml:"href"` + Propstat []PropstatUnmarshalXML `xml:"propstat"` + Status string `xml:"status,omitempty"` + Error *errors.ErrorXML `xml:"d:error"` + ResponseDescription string `xml:"responsedescription,omitempty"` +} + +// MultiStatusResponseUnmarshalXML is a workaround for https://github.com/golang/go/issues/13400 +type MultiStatusResponseUnmarshalXML struct { + XMLName xml.Name `xml:"multistatus"` + XmlnsS string `xml:"xmlns:s,attr,omitempty"` + XmlnsD string `xml:"xmlns:d,attr,omitempty"` + XmlnsOC string `xml:"xmlns:oc,attr,omitempty"` + + Responses []*ResponseUnmarshalXML `xml:"response"` +} + +// PropstatUnmarshalXML is a workaround for https://github.com/golang/go/issues/13400 +type PropstatUnmarshalXML struct { + // Prop requires DAV: to be the default namespace in the enclosing + // XML. This is due to the standard encoding/xml package currently + // not honoring namespace declarations inside a xmltag with a + // parent element for anonymous slice elements. + // Use of multistatusWriter takes care of this. + Prop []*prop.PropertyXML `xml:"prop"` + Status string `xml:"status"` + Error *errors.ErrorXML `xml:"d:error"` + ResponseDescription string `xml:"responsedescription,omitempty"` +} + +// NewMultiStatusResponseXML returns a preconfigured instance of MultiStatusResponseXML +func NewMultiStatusResponseXML() *MultiStatusResponseXML { + return &MultiStatusResponseXML{ + XmlnsD: "DAV:", + XmlnsS: "http://sabredav.org/ns", + XmlnsOC: "http://owncloud.org/ns", + } +} + +// ReadPropfind extracts and parses the propfind XML information from a Reader +// from https://github.com/golang/net/blob/e514e69ffb8bc3c76a71ae40de0118d794855992/webdav/xml.go#L178-L205 +func ReadPropfind(r io.Reader) (pf XML, status int, err error) { + c := countingReader{r: r} + if err = xml.NewDecoder(&c).Decode(&pf); err != nil { + if err == io.EOF { + if c.n == 0 { + // An empty body means to propfind allprop. + // http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND + return XML{Allprop: new(struct{})}, 0, nil + } + err = errors.ErrInvalidPropfind + } + return XML{}, http.StatusBadRequest, err + } + + if pf.Allprop == nil && pf.Include != nil { + return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind + } + if pf.Allprop != nil && (pf.Prop != nil || pf.Propname != nil) { + return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind + } + if pf.Prop != nil && pf.Propname != nil { + return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind + } + if pf.Propname == nil && pf.Allprop == nil && pf.Prop == nil { + // jfd: I think is perfectly valid ... treat it as allprop + return XML{Allprop: new(struct{})}, 0, nil + } + return pf, 0, nil +} + +// UnmarshalXML appends the property names enclosed within start to pn. +// +// It returns an error if start does not contain any properties or if +// properties contain values. Character data between properties is ignored. +func (pn *Props) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + for { + t, err := prop.Next(d) + if err != nil { + return err + } + switch e := t.(type) { + case xml.EndElement: + // jfd: I think is perfectly valid ... treat it as allprop + /* + if len(*pn) == 0 { + return fmt.Errorf("%s must not be empty", start.Name.Local) + } + */ + return nil + case xml.StartElement: + t, err = prop.Next(d) + if err != nil { + return err + } + if _, ok := t.(xml.EndElement); !ok { + return fmt.Errorf("unexpected token %T", t) + } + *pn = append(*pn, e.Name) + } + } +}