1
0

Better caldav support (#73)

This commit is contained in:
konrad
2019-05-22 17:48:48 +00:00
committed by Gitea
parent de24fcc2f8
commit 7107d030fc
91 changed files with 7060 additions and 323 deletions

36
vendor/github.com/samedi/caldav-go/handlers/builder.go generated vendored Normal file
View File

@ -0,0 +1,36 @@
package handlers
import (
"net/http"
)
// HandlerInterface represents a CalDAV request handler. It has only one function `Handle`,
// which is used to handle the CalDAV request and returns the response.
type HandlerInterface interface {
Handle() *Response
}
// NewHandler returns a new CalDAV request handler object based on the provided request.
// With the returned request handler, you can call `Handle()` to handle the request.
func NewHandler(request *http.Request) HandlerInterface {
response := NewResponse()
switch request.Method {
case "GET":
return getHandler{request, response, false}
case "HEAD":
return getHandler{request, response, true}
case "PUT":
return putHandler{request, response}
case "DELETE":
return deleteHandler{request, response}
case "PROPFIND":
return propfindHandler{request, response}
case "OPTIONS":
return optionsHandler{response}
case "REPORT":
return reportHandler{request, response}
default:
return notImplementedHandler{response}
}
}

40
vendor/github.com/samedi/caldav-go/handlers/delete.go generated vendored Normal file
View File

@ -0,0 +1,40 @@
package handlers
import (
"github.com/samedi/caldav-go/global"
"net/http"
)
type deleteHandler struct {
request *http.Request
response *Response
}
func (dh deleteHandler) Handle() *Response {
precond := requestPreconditions{dh.request}
// get the event from the storage
resource, _, err := global.Storage.GetShallowResource(dh.request.URL.Path)
if err != nil {
return dh.response.SetError(err)
}
// TODO: Handle delete on collections
if resource.IsCollection() {
return dh.response.Set(http.StatusMethodNotAllowed, "")
}
// check ETag pre-condition
resourceEtag, _ := resource.GetEtag()
if !precond.IfMatch(resourceEtag) {
return dh.response.Set(http.StatusPreconditionFailed, "")
}
// delete event after pre-condition passed
err = global.Storage.DeleteResource(resource.Path)
if err != nil {
return dh.response.SetError(err)
}
return dh.response.Set(http.StatusNoContent, "")
}

37
vendor/github.com/samedi/caldav-go/handlers/get.go generated vendored Normal file
View File

@ -0,0 +1,37 @@
package handlers
import (
"github.com/samedi/caldav-go/global"
"net/http"
)
type getHandler struct {
request *http.Request
response *Response
onlyHeaders bool
}
func (gh getHandler) Handle() *Response {
resource, _, err := global.Storage.GetResource(gh.request.URL.Path)
if err != nil {
return gh.response.SetError(err)
}
var response string
if gh.onlyHeaders {
response = ""
} else {
response, _ = resource.GetContentData()
}
etag, _ := resource.GetEtag()
lastm, _ := resource.GetLastModified(http.TimeFormat)
ctype, _ := resource.GetContentType()
gh.response.SetHeader("ETag", etag).
SetHeader("Last-Modified", lastm).
SetHeader("Content-Type", ctype).
Set(http.StatusOK, response)
return gh.response
}

27
vendor/github.com/samedi/caldav-go/handlers/headers.go generated vendored Normal file
View File

@ -0,0 +1,27 @@
package handlers
import (
"net/http"
)
const (
HD_DEPTH = "Depth"
HD_DEPTH_DEEP = "1"
HD_PREFER = "Prefer"
HD_PREFER_MINIMAL = "return=minimal"
HD_PREFERENCE_APPLIED = "Preference-Applied"
)
type headers struct {
http.Header
}
func (h headers) IsDeep() bool {
depth := h.Get(HD_DEPTH)
return (depth == HD_DEPTH_DEEP)
}
func (h headers) IsMinimal() bool {
prefer := h.Get(HD_PREFER)
return (prefer == HD_PREFER_MINIMAL)
}

View File

@ -0,0 +1,207 @@
package handlers
import (
"encoding/xml"
"fmt"
"github.com/samedi/caldav-go/data"
"github.com/samedi/caldav-go/global"
"github.com/samedi/caldav-go/ixml"
"github.com/samedi/caldav-go/lib"
"net/http"
)
// Wraps a multistatus response. It contains the set of `Responses`
// that will serve to build the final XML. Multistatus responses are
// used by the REPORT and PROPFIND methods.
type multistatusResp struct {
// The set of multistatus responses used to build each of the <DAV:response> nodes.
Responses []msResponse
// Flag that XML should be minimal or not
// [defined in the draft https://tools.ietf.org/html/draft-murchison-webdav-prefer-05]
Minimal bool
}
type msResponse struct {
Href string
Found bool
Propstats msPropstats
}
type msPropstats map[int]msProps
// Adds a msProp to the map with the key being the prop status.
func (stats msPropstats) Add(prop msProp) {
stats[prop.Status] = append(stats[prop.Status], prop)
}
func (stats msPropstats) Clone() msPropstats {
clone := make(msPropstats)
for k, v := range stats {
clone[k] = v
}
return clone
}
type msProps []msProp
type msProp struct {
Tag xml.Name
Content string
Contents []string
Status int
}
// Function that processes all the required props for a given resource.
// ## Params
// resource: the target calendar resource.
// reqprops: set of required props that must be processed for the resource.
// ## Returns
// The set of props (msProp) processed. Each prop is mapped to a HTTP status code.
// So if a prop is found and processed ok, it'll be mapped to 200. If it's not found,
// it'll be mapped to 404, and so on.
func (ms *multistatusResp) Propstats(resource *data.Resource, reqprops []xml.Name) msPropstats {
if resource == nil {
return nil
}
result := make(msPropstats)
for _, ptag := range reqprops {
pvalue := msProp{
Tag: ptag,
Status: http.StatusOK,
}
pfound := false
switch ptag {
case ixml.CALENDAR_DATA_TG:
pvalue.Content, pfound = resource.GetContentData()
if pfound {
pvalue.Content = ixml.EscapeText(pvalue.Content)
}
case ixml.GET_ETAG_TG:
pvalue.Content, pfound = resource.GetEtag()
case ixml.GET_CONTENT_TYPE_TG:
pvalue.Content, pfound = resource.GetContentType()
case ixml.GET_CONTENT_LENGTH_TG:
pvalue.Content, pfound = resource.GetContentLength()
case ixml.DISPLAY_NAME_TG:
pvalue.Content, pfound = resource.GetDisplayName()
if pfound {
pvalue.Content = ixml.EscapeText(pvalue.Content)
}
case ixml.GET_LAST_MODIFIED_TG:
pvalue.Content, pfound = resource.GetLastModified(http.TimeFormat)
case ixml.OWNER_TG:
pvalue.Content, pfound = resource.GetOwnerPath()
case ixml.GET_CTAG_TG:
pvalue.Content, pfound = resource.GetEtag()
case ixml.PRINCIPAL_URL_TG,
ixml.PRINCIPAL_COLLECTION_SET_TG,
ixml.CALENDAR_USER_ADDRESS_SET_TG,
ixml.CALENDAR_HOME_SET_TG:
pvalue.Content, pfound = ixml.HrefTag(resource.Path), true
case ixml.RESOURCE_TYPE_TG:
if resource.IsCollection() {
pvalue.Content, pfound = ixml.Tag(ixml.COLLECTION_TG, "")+ixml.Tag(ixml.CALENDAR_TG, ""), true
if resource.IsPrincipal() {
pvalue.Content += ixml.Tag(ixml.PRINCIPAL_TG, "")
}
} else {
// resourcetype must be returned empty for non-collection elements
pvalue.Content, pfound = "", true
}
case ixml.CURRENT_USER_PRINCIPAL_TG:
if global.User != nil {
path := fmt.Sprintf("/%s/", global.User.Name)
pvalue.Content, pfound = ixml.HrefTag(path), true
}
case ixml.SUPPORTED_CALENDAR_COMPONENT_SET_TG:
if resource.IsCollection() {
for _, component := range global.SupportedComponents {
// TODO: use ixml somehow to build the below tag
compTag := fmt.Sprintf(`<C:comp name="%s"/>`, component)
pvalue.Contents = append(pvalue.Contents, compTag)
}
pfound = true
}
}
if !pfound {
pvalue.Status = http.StatusNotFound
}
result.Add(pvalue)
}
return result
}
// Adds a new `msResponse` to the `Responses` array.
func (ms *multistatusResp) AddResponse(href string, found bool, propstats msPropstats) {
ms.Responses = append(ms.Responses, msResponse{
Href: href,
Found: found,
Propstats: propstats,
})
}
func (ms *multistatusResp) ToXML() string {
// init multistatus
var bf lib.StringBuffer
bf.Write(`<?xml version="1.0" encoding="UTF-8"?>`)
bf.Write(`<D:multistatus %s>`, ixml.Namespaces())
// iterate over event hrefs and build multistatus XML on the fly
for _, response := range ms.Responses {
bf.Write("<D:response>")
bf.Write(ixml.HrefTag(response.Href))
if response.Found {
propstats := response.Propstats.Clone()
if ms.Minimal {
delete(propstats, http.StatusNotFound)
if len(propstats) == 0 {
bf.Write("<D:propstat>")
bf.Write("<D:prop/>")
bf.Write(ixml.StatusTag(http.StatusOK))
bf.Write("</D:propstat>")
bf.Write("</D:response>")
continue
}
}
for status, props := range propstats {
bf.Write("<D:propstat>")
bf.Write("<D:prop>")
for _, prop := range props {
bf.Write(ms.propToXML(prop))
}
bf.Write("</D:prop>")
bf.Write(ixml.StatusTag(status))
bf.Write("</D:propstat>")
}
} else {
// if does not find the resource set 404
bf.Write(ixml.StatusTag(http.StatusNotFound))
}
bf.Write("</D:response>")
}
bf.Write("</D:multistatus>")
return bf.String()
}
func (ms *multistatusResp) propToXML(prop msProp) string {
for _, content := range prop.Contents {
prop.Content += content
}
xmlString := ixml.Tag(prop.Tag, prop.Content)
return xmlString
}

View File

@ -0,0 +1,13 @@
package handlers
import (
"net/http"
)
type notImplementedHandler struct {
response *Response
}
func (h notImplementedHandler) Handle() *Response {
return h.response.Set(http.StatusNotImplemented, "")
}

23
vendor/github.com/samedi/caldav-go/handlers/options.go generated vendored Normal file
View File

@ -0,0 +1,23 @@
package handlers
import (
"net/http"
)
type optionsHandler struct {
response *Response
}
// Returns the allowed methods and the DAV features implemented by the current server.
// For more information about the values and format read RFC4918 Sections 10.1 and 18.
func (oh optionsHandler) Handle() *Response {
// Set the DAV compliance header:
// 1: Server supports all the requirements specified in RFC2518
// 3: Server supports all the revisions specified in RFC4918
// calendar-access: Server supports all the extensions specified in RFC4791
oh.response.SetHeader("DAV", "1, 3, calendar-access").
SetHeader("Allow", "GET, HEAD, PUT, DELETE, OPTIONS, PROPFIND, REPORT").
Set(http.StatusOK, "")
return oh.response
}

View File

@ -0,0 +1,23 @@
package handlers
import (
"net/http"
)
type requestPreconditions struct {
request *http.Request
}
func (p *requestPreconditions) IfMatch(etag string) bool {
etagMatch := p.request.Header["If-Match"]
return len(etagMatch) == 0 || etagMatch[0] == "*" || etagMatch[0] == etag
}
func (p *requestPreconditions) IfMatchPresent() bool {
return len(p.request.Header["If-Match"]) != 0
}
func (p *requestPreconditions) IfNoneMatch(value string) bool {
valueMatch := p.request.Header["If-None-Match"]
return len(valueMatch) == 1 && valueMatch[0] == value
}

View File

@ -0,0 +1,49 @@
package handlers
import (
"encoding/xml"
"github.com/samedi/caldav-go/global"
"net/http"
)
type propfindHandler struct {
request *http.Request
response *Response
}
func (ph propfindHandler) Handle() *Response {
requestBody := readRequestBody(ph.request)
header := headers{ph.request.Header}
// get the target resources based on the request URL
resources, err := global.Storage.GetResources(ph.request.URL.Path, header.IsDeep())
if err != nil {
return ph.response.SetError(err)
}
// read body string to xml struct
type XMLProp2 struct {
Tags []xml.Name `xml:",any"`
}
type XMLRoot2 struct {
XMLName xml.Name
Prop XMLProp2 `xml:"DAV: prop"`
}
var requestXML XMLRoot2
xml.Unmarshal([]byte(requestBody), &requestXML)
multistatus := &multistatusResp{
Minimal: header.IsMinimal(),
}
// for each href, build the multistatus responses
for _, resource := range resources {
propstats := multistatus.Propstats(&resource, requestXML.Prop.Tags)
multistatus.AddResponse(resource.Path, true, propstats)
}
if multistatus.Minimal {
ph.response.SetHeader(HD_PREFERENCE_APPLIED, HD_PREFER_MINIMAL)
}
return ph.response.Set(207, multistatus.ToXML())
}

65
vendor/github.com/samedi/caldav-go/handlers/put.go generated vendored Normal file
View File

@ -0,0 +1,65 @@
package handlers
import (
"github.com/samedi/caldav-go/errs"
"github.com/samedi/caldav-go/global"
"net/http"
)
type putHandler struct {
request *http.Request
response *Response
}
func (ph putHandler) Handle() *Response {
requestBody := readRequestBody(ph.request)
precond := requestPreconditions{ph.request}
success := false
// check if resource exists
resourcePath := ph.request.URL.Path
resource, found, err := global.Storage.GetShallowResource(resourcePath)
if err != nil && err != errs.ResourceNotFoundError {
return ph.response.SetError(err)
}
// PUT is allowed in 2 cases:
//
// 1. Item NOT FOUND and there is NO ETAG match header: CREATE a new item
if !found && !precond.IfMatchPresent() {
// create new event resource
resource, err = global.Storage.CreateResource(resourcePath, requestBody)
if err != nil {
return ph.response.SetError(err)
}
success = true
}
if found {
// TODO: Handle PUT on collections
if resource.IsCollection() {
return ph.response.Set(http.StatusPreconditionFailed, "")
}
// 2. Item exists, the resource etag is verified and there's no IF-NONE-MATCH=* header: UPDATE the item
resourceEtag, _ := resource.GetEtag()
if found && precond.IfMatch(resourceEtag) && !precond.IfNoneMatch("*") {
// update resource
resource, err = global.Storage.UpdateResource(resourcePath, requestBody)
if err != nil {
return ph.response.SetError(err)
}
success = true
}
}
if !success {
return ph.response.Set(http.StatusPreconditionFailed, "")
}
resourceEtag, _ := resource.GetEtag()
return ph.response.SetHeader("ETag", resourceEtag).
Set(http.StatusCreated, "")
}

168
vendor/github.com/samedi/caldav-go/handlers/report.go generated vendored Normal file
View File

@ -0,0 +1,168 @@
package handlers
import (
"encoding/xml"
"fmt"
"net/http"
"strings"
"github.com/samedi/caldav-go/data"
"github.com/samedi/caldav-go/global"
"github.com/samedi/caldav-go/ixml"
)
type reportHandler struct {
request *http.Request
response *Response
}
// See more at RFC4791#section-7.1
func (rh reportHandler) Handle() *Response {
requestBody := readRequestBody(rh.request)
header := headers{rh.request.Header}
urlResource, found, err := global.Storage.GetShallowResource(rh.request.URL.Path)
if !found {
return rh.response.Set(http.StatusNotFound, "")
} else if err != nil {
return rh.response.SetError(err)
}
// read body string to xml struct
var requestXML reportRootXML
xml.Unmarshal([]byte(requestBody), &requestXML)
// The resources to be reported are fetched by the type of the request. If it is
// a `calendar-multiget`, the resources come based on a set of `hrefs` in the request body.
// If it is a `calendar-query`, the resources are calculated based on set of filters in the request.
var resourcesToReport []reportRes
switch requestXML.XMLName {
case ixml.CALENDAR_MULTIGET_TG:
resourcesToReport, err = rh.fetchResourcesByList(urlResource, requestXML.Hrefs)
case ixml.CALENDAR_QUERY_TG:
resourcesToReport, err = rh.fetchResourcesByFilters(urlResource, requestXML.Filters)
default:
return rh.response.Set(http.StatusPreconditionFailed, "")
}
if err != nil {
return rh.response.SetError(err)
}
multistatus := &multistatusResp{
Minimal: header.IsMinimal(),
}
// for each href, build the multistatus responses
for _, r := range resourcesToReport {
propstats := multistatus.Propstats(r.resource, requestXML.Prop.Tags)
multistatus.AddResponse(r.href, r.found, propstats)
}
if multistatus.Minimal {
rh.response.SetHeader(HD_PREFERENCE_APPLIED, HD_PREFER_MINIMAL)
}
return rh.response.Set(207, multistatus.ToXML())
}
type reportPropXML struct {
Tags []xml.Name `xml:",any"`
}
type reportRootXML struct {
XMLName xml.Name
Prop reportPropXML `xml:"DAV: prop"`
Hrefs []string `xml:"DAV: href"`
Filters reportFilterXML `xml:"urn:ietf:params:xml:ns:caldav filter"`
}
type reportFilterXML struct {
XMLName xml.Name
InnerContent string `xml:",innerxml"`
}
func (rfXml reportFilterXML) toString() string {
return fmt.Sprintf("<%s>%s</%s>", rfXml.XMLName.Local, rfXml.InnerContent, rfXml.XMLName.Local)
}
// Wraps a resource that has to be reported, either fetched by filters or by a list.
// Basically it contains the original requested `href`, the actual `resource` (can be nil)
// and if the `resource` was `found` or not
type reportRes struct {
href string
resource *data.Resource
found bool
}
// The resources are fetched based on the origin resource and a set of filters.
// If the origin resource is a collection, the filters are checked against each of the collection's resources
// to see if they match. The collection's resources that match the filters are returned. The ones that will be returned
// are the resources that were not found (does not exist) and the ones that matched the filters. The ones that did not
// match the filter will not appear in the response result.
// If the origin resource is not a collection, the function just returns it and ignore any filter processing.
// [See RFC4791#section-7.8]
func (rh reportHandler) fetchResourcesByFilters(origin *data.Resource, filtersXML reportFilterXML) ([]reportRes, error) {
// The list of resources that has to be reported back in the response.
reps := []reportRes{}
if origin.IsCollection() {
filters, _ := data.ParseResourceFilters(filtersXML.toString())
resources, err := global.Storage.GetResourcesByFilters(origin.Path, filters)
if err != nil {
return reps, err
}
for in, resource := range resources {
reps = append(reps, reportRes{resource.Path, &resources[in], true})
}
} else {
// the origin resource is not a collection, so returns just that as the result
reps = append(reps, reportRes{origin.Path, origin, true})
}
return reps, nil
}
// The hrefs can come from (1) the request URL or (2) from the request body itself.
// If the origin resource from the URL points to a collection (2), we will check the request body
// to get the requested `hrefs` (resource paths). Each requested href has to be related to the collection.
// The ones that are not, we simply ignore them.
// If the resource from the URL is NOT a collection (1) we process the the report only for this resource
// and ignore any othre requested hrefs that might be present in the request body.
// [See RFC4791#section-7.9]
func (rh reportHandler) fetchResourcesByList(origin *data.Resource, requestedPaths []string) ([]reportRes, error) {
reps := []reportRes{}
if origin.IsCollection() {
resources, err := global.Storage.GetResourcesByList(requestedPaths)
if err != nil {
return reps, err
}
// we put all the resources found in a map path -> resource.
// this will be used later to query which requested resource was found
// or not and mount the response
resourcesMap := make(map[string]*data.Resource)
for _, resource := range resources {
r := resource
resourcesMap[resource.Path] = &r
}
for _, requestedPath := range requestedPaths {
// if the requested path does not belong to the origin collection, skip
// ('belonging' means that the path's prefix is the same as the collection path)
if !strings.HasPrefix(requestedPath, origin.Path) {
continue
}
resource, found := resourcesMap[requestedPath]
reps = append(reps, reportRes{requestedPath, resource, found})
}
} else {
reps = append(reps, reportRes{origin.Path, origin, true})
}
return reps, nil
}

View File

@ -0,0 +1,72 @@
package handlers
import (
"github.com/samedi/caldav-go/errs"
"io"
"net/http"
)
// Response represents the handled CalDAV response. Used this when one needs to proxy the generated
// response before being sent back to the client.
type Response struct {
Status int
Header http.Header
Body string
Error error
}
// NewResponse initializes a new response object.
func NewResponse() *Response {
return &Response{
Header: make(http.Header),
}
}
// Set sets the the status and body of the response.
func (r *Response) Set(status int, body string) *Response {
r.Status = status
r.Body = body
return r
}
// SetHeader adds a header to the response.
func (r *Response) SetHeader(key, value string) *Response {
r.Header.Set(key, value)
return r
}
// SetError sets the response as an error. It inflects the response status based on the provided error.
func (r *Response) SetError(err error) *Response {
r.Error = err
switch err {
case errs.ResourceNotFoundError:
r.Status = http.StatusNotFound
case errs.UnauthorizedError:
r.Status = http.StatusUnauthorized
case errs.ForbiddenError:
r.Status = http.StatusForbidden
default:
r.Status = http.StatusInternalServerError
}
return r
}
// Write writes the response back to the client using the provided `ResponseWriter`.
func (r *Response) Write(writer http.ResponseWriter) {
if r.Error == errs.UnauthorizedError {
r.SetHeader("WWW-Authenticate", `Basic realm="Restricted"`)
}
for key, values := range r.Header {
for _, value := range values {
writer.Header().Set(key, value)
}
}
writer.WriteHeader(r.Status)
io.WriteString(writer, r.Body)
}

18
vendor/github.com/samedi/caldav-go/handlers/shared.go generated vendored Normal file
View File

@ -0,0 +1,18 @@
package handlers
import (
"bytes"
"io/ioutil"
"net/http"
)
// This function reads the request body and restore its content, so that
// the request body can be read a second time.
func readRequestBody(request *http.Request) string {
// Read the content
body, _ := ioutil.ReadAll(request.Body)
// Restore the io.ReadCloser to its original state
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
// Use the content
return string(body)
}