Let's say that you help managing a small organisation or club and have a database storing all the members details (names, phone, email...).
Wouldn't it be nice to have access to this up-to-date information everywhere you need it? Well, with CardDAV you can!
CardDAV is a well-supported open standard for contact management; it has a native integration in the iOS Contacts App and many apps available for Android.
Server-side, implementing CardDAV is an http-server which responds to unusual http-methods (PROPFIND, REPORT instead of GET, POST...). Fortunately there exist a Go module to greatly simplify the work: github.com/emersion/go-webdav. This library expects an implemented Backend and provides a standard http.Handler which should serve HTTP requests after authentication.
Interestingly the library does not provide any help regarding user authentication, however thanks to Go composability, this is not an issue.
CardDAV uses Basic Auth credentials. Once the credentials are checked, we can save those credentials in the context (will be useful later):
package main import ( "context" "net/http" "github.com/emersion/go-webdav/carddav" ) type ( ctxKey struct{} ctxValue struct { username string } ) func NewCardDAVHandler() http.Handler { actualHandler := carddav.Handler{ Backend: &ownBackend{}, } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() // check username and password: adjust the logic to your system (do NOT store passwords in plaintext) if !ok || username != "admin" || password != "s3cr3t" { // abort the request handling on failure w.Header().Add("WWW-Authenticate", `Basic realm="Please authenticate", charset="UTF-8"`) http.Error(w, "HTTP Basic auth is required", http.StatusUnauthorized) return } // user is authenticated: store this info in the context ctx := context.WithValue(r.Context(), ctxKey{}, ctxValue{username}) // delegate the work to the CardDAV handle actualHandler.ServeHTTP(w, r.WithContext(ctx)) }) }
The ownBackend struct must implement the carddav.Backend interface, which is not very thin, but still manageable.
The CurrentUserPrincipal and AddressBookHomeSetPath must provide URLs (starting and ending with a slash). Usually it will be username/contacts. This is where you need to extract the username from the context (which is the only available argument):
func currentUsername(ctx context.Context) (string, error) { if v, ok := ctx.Value(ctxKey{}).(ctxValue); ok { return v.username, nil } return "", errors.New("not authenticated") } type ownBackend struct{} // must begin and end with a slash func (b *ownBackend) CurrentUserPrincipal(ctx context.Context) (string, error) { username, err := currentUsername(ctx) return "/" + url.PathEscape(username) + "/", err } // must begin and end with a slash as well func (b *ownBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) { principal, err := b.CurrentUserPrincipal(ctx) return principal + "contacts/", err }
After that the fun can begin: you need to implement the AddressBook, GetAddressObject and ListAddressObjects methods.
AddressBook returns a simple struct, where path should start with the AddressBookHomeSetPath above (and end with a slash)
GetAddressObject and ListAddressObjects must check the current path (to ensure that the currently authenticated user can access those contacts) and then return the contacts as AddressObject.
The AddressObject has multiple attributes, most importantly:
The VCard represents the actual contact data and must likely be adapted depending on how you store your contacts. In my case, it ended like this:
func utf8Field(v string) *vcard.Field { return &vcard.Field{ Value: v, Params: vcard.Params{ "CHARSET": []string{"UTF-8"}, }, } } func vcardFromUser(u graphqlient.User) vcard.Card { c := vcard.Card{} c.Set(vcard.FieldFormattedName, utf8Field(u.Firstname+" "+u.Lastname)) c.SetName(&vcard.Name{ Field: utf8Field(""), FamilyName: u.Lastname, GivenName: u.Firstname, }) c.SetRevision(u.UpdatedAt) c.SetValue(vcard.FieldUID, u.Extid) c.Set(vcard.FieldOrganization, utf8Field(u.Unit)) // addFields sorts the key to ensure a stable order addFields := func(fieldName string, values map[string]string) { for _, k := range slices.Sorted(maps.Keys(values)) { v := values[k] c.Add(fieldName, &vcard.Field{ Value: v, Params: vcard.Params{ vcard.ParamType: []string{k + ";CHARSET=UTF-8"}, // hacky but prevent maps ordering issues // "CHARSET": []string{"UTF-8"}, }, }) } } addFields(vcard.FieldEmail, u.Emails) addFields(vcard.FieldTelephone, u.Phones) vcard.ToV4(c) return c }
Some methods allow to update a contact. Since I don't want my member list to be updated via CardDAV, I return a 403 error to the Put and Delete methods: return webdav.NewHTTPError(http.StatusForbidden, errors.New("carddav: operation not supported"))
iOS requires the CardDAV server to serve over https. You can generate self-signed certificates locally using openssl (replace 192.168.XXX.XXX with your IP address) to be fed into http.ListenAndServeTLS(addr, "localhost.crt", "localhost.key", NewCardDAVHandler())
openssl req -new -subj "/C=US/ST=Utah/CN=192.168.XXX.XXX" -newkey rsa:2048 -nodes -keyout localhost.key -out localhost.csr openssl x509 -req -days 365 -in localhost.csr -signkey localhost.key -out localhost.crt
After that you should be able to experiment locally by adding a "CardDAV contact account" pointing to your own IP-Address and port.
Implementing a CardDAV server in Go is a bit involved, but clearly worth it: your contacts will automatically be in sync with the data you have on your organisation's server!
Do you know other cool protocols which allow this kind of native integration? Feel free to share your experiences!
The above is the detailed content of How to synchronize your contacts with your phone? Implemeting CardDAV in Go!. For more information, please follow other related articles on the PHP Chinese website!