Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve service discovery, implement SD for CalDav #42

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions caldav/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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{
Expand Down
33 changes: 3 additions & 30 deletions carddav/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"mime"
"net"
"net/http"
"net/url"
"strconv"
Expand All @@ -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.
dpeukert marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down
86 changes: 86 additions & 0 deletions internal/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,99 @@ import (
"fmt"
"io"
"mime"
"net"
"net/http"
"net/url"
"path"
"strings"
"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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC 6764 section 6 mandates that a PROPFIND request should be made with the username/password set.

Alps has a different requirement: it needs to sanity check the URLs on startup (without any username/password).

To follow the RFC, we could:

  • Take an webdav.HTTPClient to handle auth
  • Change OPTIONS to a PROPFIND request with a DAV:current-user-principal prop
  • Return the DAV:current-user-principal prop

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To accomodate for Alps, we could have an intermediate DiscoverContextURL function that follows the RFC up to and not including the PROPFIND request.

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)
}
dpeukert marked this conversation as resolved.
Show resolved Hide resolved

return serviceUrl, nil
}

// HTTPClient performs HTTP requests. It's implemented by *http.Client.
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
Expand Down