首页 >后端开发 >Golang >How to synchronize your contacts with your phone? Implemeting CardDAV in Go!

How to synchronize your contacts with your phone? Implemeting CardDAV in Go!

DDD
DDD原创
2024-09-19 22:21:191163浏览

How to synchronize your contacts with your phone? Implemeting CardDAV in Go!

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.

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))
    })
}

Implementing the CardDAV interface

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.

AddressObject

The AddressObject has multiple attributes, most importantly:

  • the path to identify this particular contact (can be arbitrary, start with a slash)
  • the ETag to allow the client to quickly check if any update happened (if you forget it, iOS won't show anything)
  • the Card which expects a VCard

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
}

Taking the Readonly-shortcut

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"))

Testing locally

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.

Conclusion

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!

以上是How to synchronize your contacts with your phone? Implemeting CardDAV in Go!的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn