From 27d90a30c5b6ebad74817b53c01a965de87abc6a Mon Sep 17 00:00:00 2001 From: Daniel Peukert Date: Tue, 13 Oct 2020 16:49:50 +0200 Subject: [PATCH] Improve service discovery, implement SD for CalDav --- caldav/client.go | 10 ++++-- carddav/client.go | 33 ++---------------- internal/client.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 32 deletions(-) diff --git a/caldav/client.go b/caldav/client.go index 8f7952e..816d281 100644 --- a/caldav/client.go +++ b/caldav/client.go @@ -15,7 +15,13 @@ import ( "github.com/emersion/go-webdav/internal" ) -// Client provides access to a remote CardDAV server. +// Discover performs a DNS-based CalDAV service discovery as described in +// RFC 6764 section 6. It returns the URL to the CalDAV server. +func Discover(host string) (string, error) { + return internal.Discover("caldav", host) +} + +// Client provides access to a remote CalDAV server. type Client struct { *webdav.Client @@ -91,7 +97,7 @@ func (c *Client) FindCalendars(calendarHomeSet string) ([]Calendar, error) { return nil, err } if maxResSize.Size < 0 { - return nil, fmt.Errorf("carddav: max-resource-size must be a positive integer") + return nil, fmt.Errorf("caldav: max-resource-size must be a positive integer") } l = append(l, Calendar{ diff --git a/carddav/client.go b/carddav/client.go index a98dc80..d6cf939 100644 --- a/carddav/client.go +++ b/carddav/client.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "mime" - "net" "net/http" "net/url" "strconv" @@ -17,35 +16,9 @@ import ( ) // Discover performs a DNS-based CardDAV service discovery as described in -// RFC 6352 section 11. It returns the URL to the CardDAV server. -func Discover(domain string) (string, error) { - // Only lookup carddavs (not carddav), plaintext connections are insecure - _, addrs, err := net.LookupSRV("carddavs", "tcp", domain) - if dnsErr, ok := err.(*net.DNSError); ok { - if dnsErr.IsTemporary { - return "", err - } - } else if err != nil { - return "", err - } - - if len(addrs) == 0 { - return "", fmt.Errorf("carddav: domain doesn't have an SRV record") - } - addr := addrs[0] - - target := strings.TrimSuffix(addr.Target, ".") - if target == "" { - return "", fmt.Errorf("carddav: empty target in SRV record") - } - - u := url.URL{Scheme: "https"} - if addr.Port == 443 { - u.Host = target - } else { - u.Host = fmt.Sprintf("%v:%v", target, addr.Port) - } - return u.String(), nil +// RFC 6764 section 6. It returns the URL to the CardDAV server. +func Discover(host string) (string, error) { + return internal.Discover("carddav", host) } // Client provides access to a remote CardDAV server. diff --git a/internal/client.go b/internal/client.go index 3b48ecf..45cfff4 100644 --- a/internal/client.go +++ b/internal/client.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "mime" + "net" "net/http" "net/url" "path" @@ -13,6 +14,91 @@ import ( "unicode" ) +// Discover performs a DNS-based CalDAV/CardDAV service discovery as described +// in RFC 6764 section 6. It returns the URL to the CalDAV/CardDAV server. +func Discover(service string, host string) (string, error) { + if service != "caldav" && service != "carddav" { + return "", fmt.Errorf("webdav: service discovery of type %v not supported", service) + } + + path := "" + + // Check for SRV records for the service we want, only lookup secure versions + // (caldavs, carddavs), plaintext connections are insecure + _, addrs, err := net.LookupSRV(fmt.Sprintf("%vs", service), "tcp", host) + if dnsErr, ok := err.(*net.DNSError); ok { + if dnsErr.IsTemporary { + return "", err + } + } else if err != nil { + return "", err + } + + if len(addrs) > 0 { + srvTarget := strings.TrimSuffix(addrs[0].Target, ".") + + // If we found one, check for a TXT record specifying the path + if srvTarget != "" { + txtRecs, err := net.LookupTXT(fmt.Sprintf("_%vs._tcp.%v", service, host)) + if dnsErr, ok := err.(*net.DNSError); ok { + if dnsErr.IsTemporary { + return "", err + } + } else if err != nil { + return "", err + } + + for _, txtRec := range txtRecs { + // This is not correct according to RFC 6763 sections 6.3 to 6.5, + // but LookupTXT merges all constituent strings together + for _, txtRecKeyVal := range strings.Split(txtRec, " ") { + if strings.HasPrefix(strings.ToLower(txtRecKeyVal), "path=") { + path = txtRecKeyVal[5:] + break + } + } + + if path != "" { + break + } + } + + if addrs[0].Port == 443 { + host = srvTarget + } else { + host = fmt.Sprintf("%v:%v", srvTarget, addrs[0].Port) + } + } + } + + // If we didn't get a path from TXT records, use the default well-known location + if path == "" { + path = fmt.Sprintf("/.well-known/%v", service) + } + + u := url.URL{Scheme: "https", Host: host, Path: path} + serviceUrl := u.String() + + // Check if the resulting URL hosts a service + req, err := http.NewRequest(http.MethodOptions, serviceUrl, nil) + if err != nil { + return "", err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + resp.Body.Close() + + // Servers might require authentication to perform an OPTIONS request + if resp.StatusCode/100 != 2 && resp.StatusCode != http.StatusUnauthorized { + return "", fmt.Errorf("HTTP request to %v failed: %v %v", serviceUrl, resp.StatusCode, resp.Status) + } + + return serviceUrl, nil +} + // HTTPClient performs HTTP requests. It's implemented by *http.Client. type HTTPClient interface { Do(req *http.Request) (*http.Response, error)