A Go library for WebDAV, CalDAV and CardDAV.
MIT
A Go library for WebDAV, CalDAV and CardDAV
License: MIT License
Add the ability to fetch the "me-card" when retrieving the address book using FindAddressBooks
FindAddressBookHomeSet
returns only the path of the URL returned from the PROPFIND request. However, iCloud returns a different host name as well as the path. This is then discarded. Trying to retrieve any address objects will then fail with an error as the wrong host is being queried.
Looking at other carddav servers this is not normal behaviour but there doesn't seem to be anything in RFC 6352 that disallows it.
ValidateCalendarObject
is doing the CalDAV checks but is missing the iCalendar checks.
go-ical has a function for this but it's not public: https://github.com/emersion/go-ical/blob/0864dccc089f7438bd942705f2dfc934e39536ca/encoder.go#L11
The current library design makes it very difficult to properly support WebDAV extensions such as CardDAV and CalDAV. It's also completely missing a client implementation.
After reviewing prior art, in the last few days I've began rewriting the library from the ground up in the next
branch. I have a minimal CardDAV client and WebDAV server working.
My plan is to release v0.1 with the current code so that existing users don't break, then release 0.2 with the rewrite. I'll start closing old issues once 0.1 is released.
For v0.2 I'd like to have:
webdav
library, exposing a filesystem-like APIcarddav
library, exposing a CardDAV-friendly APIinternal
library which can be used from webdav
and carddav
exposing low-level details (e.g. XML props)How the internal
library should look like is not clear yet, at least for the server part.
Stop checking for e.g. os.IsNotExist
in the server code. Do it in LocalFileSystem
instead.
if the project tries to adhere strictly to the RFC and implementing custom properties like X-APPLE-SORT-ORDER
is out of scope, a way to ignore it instead of failing would be nice
During e.g. a DELETE or COPY operation, the server can return a multistatus response. We need to handle it and check whether there are any failed responses in it.
#64 has extended query filter types. We need to marshal the new fields properly.
Are you considering support for https://tools.ietf.org/html/rfc6352#section-9.3 ?
I just have used your repo but I found that stat/ readdir does not support read the modified date of folder. It only work for file
Maybe we should consider using another library.
handlePropfind
errors out if a PROPFIND request is sent without a body, I think. We should parse that as an allprop
request:
A client may choose not to submit a request body. An empty PROPFIND request body MUST be treated as if it were an 'allprop' request.
internal.Response.Status
is not being updated and stays nil. The information about the entry status is present in internal.Response.Propstat[0].Status
instead
Right now we have a workaround for trailing slashes in dir URLs:
Line 461 in 9e23289
Would be nice to find a better long-term solution.
It may be desirable to have a base WebDAV filesystem, with some CalDAV/CardDAV endpoints. This should work fine except for principals. We need to figure out a way to share principals between servers.
This library needs a CalDAV client & server implementation.
https://tools.ietf.org/html/rfc4791
The FileSystem
interface is tied to the OS rather than the WebDAV protocol. This is too low-level.
The backend should perform the recursive operation, we shouldn't do it in the server.
Set the Location
response header in this case.
Right now addressbook-multiget
request will result in many GetAddressObject
calls. This is not great for backends which need to fetch resources from a remote server.
Probably in internal.Client.Do
If a server write operation failed, send a multistatus instead of an HTTP error.
I'm looking at the following code and I'm wondering whether it makes sense:
Lines 308 to 315 in 13fa812
From what I see multiGet
cannot be nil, because &multiGet.DataRequest
would throw an invalid memory address / nil pointer error. Hence the check if multiGet == nil
seems kind of pointless here.
Either that check should happen earlier or not at all, imho.
If the server returns query parameters in a href, it'll get stripped. Does it matter?
If it does, we'll need to expose href *url.URL
values instead of path string
.
As came up several times, the code keeps getting more and more complicated due to the fact that we support different path layouts (as defined below). The latest example for this is #99. While that PR does a lot of the work already, the complexity of adding features (such as multiple address books might) will always be much higher when accounting for support for all use cases.
I figured it might be wise to take a step back and reiterate how we got here, what problems we are actually trying to solve, and if there is maybe different approach altogether that could ease the pain here.
Let's start with some definitions: WebDAV/CardDAV/CalDAV define a set of paths that serve certain special purposes. We need to consider the following:
go-webdav
only supports a single calendar/address book. This URL has a relation to the calendar/address book home set: it should be underneath it (or at least equal to it), to be discoverable.For brevity, I will from now on only focus on CalDAV, but the same applies of course to CardDAV.
Where we started
Initially, the server was designed in such a way that everything was served at the root path (/
). The root path was user principal URL, calendar home set, and calendar URL at the same time. The only other URLs involved were the individual resources (events). This works, but has the following drawbacks:
/calendar1/
etc./contacts/
and /calendars/
./bitfehler/
or /emersion/
.Current status
Right now, we try to still the support the old case of everything at /
, and everything in between that and the most extreme opposite, where everything has it's own URL, such as /bitfehler/calendars/work/
, where /bitfehler/
is a user pricipal URL, /bitfehler/calendars/
is the calendar home set, and /bitfehler/calendars/work/
is an actual calendar.
Keeping support for everything has caused the regression that #99 tries to fix and leads to all this complicated code where we have to check if something is e.g. also a calendar home set, even though we already know it's a user principal URL.
What now?
Writing all this down, I think my proposal would be to go with the layout that makes everything possible (/bitfehler/calendars/work/
), but only support that. That should make the code much clearer again, while still allowing to add any feature we might want in the future. It would make writing a backend a tiny bit more work, but I think if all you need is one user and one calendar, hard-coding some names the backend is not too much to ask.
If you think this might be a route worth pursuing, I can offer to create a PR for that, to see what the code might look like?
Right now backends can only support a single address book.
Hi!
Currently caldav documentation does not mention server.go. Will it be added ?
https://pkg.go.dev/github.com/emersion/[email protected]/caldav
If the backend returns an io.Seeker
, keep the current behavior (which allows us to support byte ranges).
It's too easy to forget encoding those otherwise.
Related: #14
Is it fine to strip the query from the href?
hi,
trying to retrieve the list of contacts (served by hydroxide
) I noticed the internal.Time
value XML encoding/decoding roundtrip was broken:
time.RFC1123Z
net/http.ParseTime
that only has: [http.TimeFormat, time.RFC850, time.ANSIC]
as a list of possible time formats.Hi, I'm trying to list the events in my calendar using your library. I've managed to get as far as the list of calendars, but I can't seem to get to the actual events. Can you please have a look and let me know what I'm doing wrong?
package main
import (
"fmt"
"log"
"net/http"
gowebdav "github.com/emersion/go-webdav"
"github.com/emersion/go-webdav/caldav"
)
func main() {
fmt.Println("hello world!")
httpClient := &http.Client{}
authorizedClient := gowebdav.HTTPClientWithBasicAuth(httpClient, "USERNAME", "PASSWORD") // FIXME
caldavClient, err := caldav.NewClient(authorizedClient, "https://caldav.fastmail.com/dav/calendars/user/USERNAME")
if err != nil {
log.Fatalf("NewClient: %s", err)
}
// curl -X PROPFIND -u USERNAME:PASSWORD https://caldav.fastmail.com/dav/principals/user/USERNAME
homeSet, err := caldavClient.FindCalendarHomeSet("/dav/principals/user/USERNAME")
if err != nil {
log.Fatalf("FindCalendarHomeSet: %s", err)
}
fmt.Println(homeSet)
calendars, err := caldavClient.FindCalendars(homeSet)
if err != nil {
log.Fatalf("FindCalendars: %s", err)
}
for i, calendar := range calendars {
fmt.Printf("cal %d: %s %s\n", i, calendar.Name, calendar.Path)
}
compRequest := caldav.CalendarCompRequest{
Name: "VEVENT",
AllProps: true,
AllComps: true,
}
compFilter := caldav.CompFilter{}
query := caldav.CalendarQuery{CompRequest: compRequest, CompFilter: compFilter}
objects, err := caldavClient.QueryCalendar(calendars[3].Path, &query)
if err != nil {
log.Fatalf("QueryCalendar: %s", err)
}
for i, obj := range objects {
fmt.Printf("%d %s", i, obj.Path)
}
}
I'm getting this error:
2022/04/18 13:21:01 QueryCalendar: 400 Bad Request: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>400 Bad Request</title>
</head>
<body>
<h1>Bad Request</h1>
<p>The request was not understood by this server.</p>
<hr>
<address>Cyrus-HTTP/3.7.0-alpha0-387-g7ea99c4045-fm-20220413.002-g7ea99c40 Cyrus-SASL/2.1.27 Lib/XML2.9.10 Jansson/2.13.1 OpenSSL/1.1.1n Wslay/1.1.1 Zlib/1.2.11 Brotli/1.0.9 Xapian/1.5.0 LibiCal/3.0 ICU4C/69.1 SQLite/3.34.1 Server at caldav.fastmail.com Port 2275</address>
</body>
</html>
exit status 1
Also, if there are any docs/tutorials on using your library?
Just a suggestion: I was looking to potentially use this project instead of golang.org/x/net/webdav
and noticed the FileSystem
interface is slightly different:
go-webdav
doesn't pass a Context
to its calls.FileInfo
is a struct and not an interface.Readdir
returns a []FileInfo
(slice of values) but Stat
returns a *FileInfo
(pointer).I was wondering if you'd consider changing this in the future? Specifically, the first two are useful for when a file system is actually remote like e.g. Dropbox or Google Drive. And using pointers in the slice is just good for consistency (and also, deep copying a slice of values has a larger overhead).
If there's no SRV record, maybe we should default to the input domain, as suggested in the RFC?
* If an SRV record is not found, the client will need to prompt
the user to enter the FQDN and port number information
directly or use some other heuristic, for example, using the
extracted "domain" as the FQDN and default HTTPS or HTTP port
numbers.
Or at least return a fixed error value which can be handled by the user.
Low-level support for Collection Synchronizations as described in This RFC
It's stateful, it's tied to os
and it's too restrictive (we don't actually need io.Seeker
).
Replace it with something like:
type File interface {
io.ReadCloser
}
type FileSystem interface {
Open(name string) (File, error)
Stat(name string) (os.FileInfo, error)
Readdir(name string) ([]os.FileInfo, error)
}
I noticed that icloud doesn't add the GetLastModified property on addressbook-multiget
and addrssbook-query
calls, however decodeAddressList()
see code here returns error when not exists.
Hello, we've recently come across your project and are very interested in using it.
However despite the large count of contributors and people using this library we've failed to find any source of documentation.
Does your project have documentation? If yes, could this possibly be pointed to somehow?
I experimented a bit with this library and found an issue with caldavClient.QueryCalendar.
I am trying to query a calendar from OpenXchange but I get the error "webdav: failed to unquote ETag: invalid syntax". This error seems to come from the file "internal/elements.go:345" and occurs because in the returned event the ETag looks like this: "<D:getetag>http://www.open-xchange.com/etags/6063-1626334223127</D:getetag>".
Unfortunately, I don't know how to fix this. Should you need more information, I will of course make it available.
Program source
package main
import (
"fmt"
"github.com/emersion/go-webdav"
"github.com/emersion/go-webdav/caldav"
"log"
"net/http"
"net/http/httptrace"
"net/http/httputil"
"os"
"time"
)
// transport is an http.RoundTripper that keeps track of the in-flight
// request and implements hooks to report HTTP tracing events.
type transport struct {
current *http.Request
}
// RoundTrip wraps http.DefaultTransport.RoundTrip to keep track
// of the current request.
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
t.current = req
fmt.Println(req)
resp, err := http.DefaultTransport.RoundTrip(req)
dump, _ := httputil.DumpResponse(resp, true)
fmt.Printf("\n\nResponse:\n%q\n", dump)
return resp, err
}
func main() {
t := &transport{}
client := &http.Client{Transport: t}
baHttpClient := webdav.HTTPClientWithBasicAuth(client, os.Getenv("USERNAME1"), os.Getenv("PASSWORD"))
caldavClient, err := caldav.NewClient(baHttpClient, os.Getenv("ENDPOINT"))
if err != nil {
log.Fatal(err)
}
principal, err := caldavClient.FindCurrentUserPrincipal()
if err != nil {
log.Fatal(err)
}
fmt.Println(principal)
homeSet, err := caldavClient.FindCalendarHomeSet(principal)
if err != nil {
log.Fatal(err)
}
fmt.Println(homeSet)
calendars, err := caldavClient.FindCalendars(homeSet)
if err != nil {
log.Fatal(err)
}
fmt.Println(calendars)
query := &caldav.CalendarQuery{
CompRequest: caldav.CalendarCompRequest{
Name: "VCALENDAR",
Props: []string{"VERSION"},
Comps: []caldav.CalendarCompRequest{{
Name: "VEVENT",
Props: []string{
"SUMMARY",
"UID",
"DTSTART",
"DTEND",
"DURATION",
},
}},
},
CompFilter: caldav.CompFilter{
Name: "VCALENDAR",
Comps: []caldav.CompFilter{{
Name: "VEVENT",
Start: time.Now().Add(-92 * time.Hour),
End: time.Now().Add(24 * time.Hour),
}},
},
}
cal, err := caldavClient.QueryCalendar(calendars[0].Path, query)
if err != nil {
log.Fatal(err)
}
fmt.Println(cal)
}
Example Response:
[...]
"HTTP/2.0 207 Multi-Status\r\nConnection: close\r\nContent-Type: text/xml; charset=UTF-8\r\nDate: Sat, 17 Jul 2021 12:36:20 GMT\r\nServer: nginx\r\nStrict-Transport-Security: max-age=63072000;\r\nX-Robots-Tag: none\r\n\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<D:multistatus xmlns:D=\"DAV:\" xmlns:APPLE=\"http://apple.com/ns/ical/\" xmlns:CAL=\"urn:ietf:params:xml:ns:caldav\" xmlns:CS=\"http://calendarserver.org/ns/\"><D:response><D:href>/caldav/283/1bbda1e5-874d-460e-8114-720623dfdd7f.ics</D:href><D:propstat><D:prop><calendar-data xmlns=\"urn:ietf:params:xml:ns:caldav\"><![CDATA[BEGIN:VCALENDAR\nPRODID:Open-Xchange\nVERSION:2.0\nCALSCALE:GREGORIAN\nBEGIN:VTIMEZONE\nTZID:Europe/Berlin\nTZURL:http://tzurl.org/zoneinfo-outlook/Europe/Berlin\nX-LIC-LOCATION:Europe/Berlin\nBEGIN:DAYLIGHT\nTZOFFSETFROM:+0100\nTZOFFSETTO:+0200\nTZNAME:CEST\nDTSTART:19700329T020000\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\nEND:DAYLIGHT\nBEGIN:STANDARD\nTZOFFSETFROM:+0200\nTZOFFSETTO:+0100\nTZNAME:CET\nDTSTART:19701025T030000\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nDTSTAMP:20210717T100023Z\nSUMMARY:TestEvent\nDTSTART;VALUE=DATE:20150120\nDTEND;VALUE=DATE:20150121\nCLASS:PUBLIC\nX-MICROSOFT-CDO-BUSYSTATUS:FREE\nTRANSP:TRANSPARENT\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=1;BYMONTHDAY=20\nUID:1bbda1e5-874d-460e-8114-720623dfdd7f\nCREATED:20150114T083240Z\nLAST-MODIFIED:20150114T083240Z\nSEQUENCE:0\nEND:VEVENT\nEND:VCALENDAR\n]]></calendar-data><D:getetag>http://www.open-xchange.com/etags/490-1421224360501</D:getetag><D:getlastmodified xmlns:b=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\" b:dt=\"dateTime.rfc1123\">Wed, 14 Jan 2015 08:32:40 GMT</D:getlastmodified></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response><D:response><D:href>/caldav/283/af3262c2-e63f-418b-946d-6e1399167a19.ics</D:href><D:propstat><D:prop><calendar-data xmlns=\"urn:ietf:params:xml:ns:caldav\"><![CDATA[BEGIN:VCALENDAR\nPRODID:Open-Xchange\nVERSION:2.0\nCALSCALE:GREGORIAN\nBEGIN:VTIMEZONE\nTZID:Europe/Berlin\nTZURL:http://tzurl.org/zoneinfo-outlook/Europe/Berlin\nX-LIC-LOCATION:Europe/Berlin\nBEGIN:DAYLIGHT\nTZOFFSETFROM:+0100\nTZOFFSETTO:+0200\nTZNAME:CEST\nDTSTART:19700329T020000\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\nEND:DAYLIGHT\nBEGIN:STANDARD\nTZOFFSETFROM:+0200\nTZOFFSETTO:+0100\nTZNAME:CET\nDTSTART:19701025T030000\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\nEND:STANDARD\nEND:VTIMEZONE\nBEGIN:VEVENT\nDTSTAMP:20210717T100023Z\nSUMMARY:TestEvent2\nDTSTART;TZID=Europe/Berlin:20210715T160000\nDTEND;TZID=Europe/Berlin:20210715T170000\nCLASS:PUBLIC\nLOCATION:TestLocation2\nX-MICROSOFT-CDO-BUSYSTATUS:BUSY\nTRANSP:OPAQUE\nUID:af3262c2-e63f-418b-946d-6e1399167a19\nCREATED:20210715T072804Z\nLAST-MODIFIED:20210715T073023Z\nSEQUENCE:1\nBEGIN:VALARM\nTRIGGER:-PT60M\nACTION:DISPLAY\nDESCRIPTION:Alarm\nACKNOWLEDGED:20210715T125900Z\nX-MOZ-LASTACK:20210715T125900Z\nEND:VALARM\nEND:VEVENT\nEND:VCALENDAR\n]]></calendar-data><D:getetag>http://www.open-xchange.com/etags/6063-1626334223127</D:getetag><D:getlastmodified xmlns:b=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\" b:dt=\"dateTime.rfc1123\">Thu, 15 Jul 2021 07:30:23 GMT</D:getlastmodified></D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat></D:response></D:multistatus>\r\n"
2021/07/17 12:36:20 webdav: failed to unquote ETag: invalid syntax
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.