Maison  >  Article  >  développement back-end  >  Comment synchroniser vos contacts avec votre téléphone ? Implémentation de CardDAV dans Go!

Comment synchroniser vos contacts avec votre téléphone ? Implémentation de CardDAV dans Go!

DDD
DDDoriginal
2024-09-19 22:21:191162parcourir

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

Disons que vous aidez à gérer une petite organisation ou un club et que vous disposez d'une base de données stockant tous les détails des membres (noms, téléphone, email...).
Ne serait-il pas agréable d'avoir accès à ces informations actualisées partout où vous en avez besoin ? Eh bien, avec CardDAV, vous pouvez !

CardDAV est un standard ouvert bien pris en charge pour la gestion des contacts ; il dispose d'une intégration native dans l'application iOS Contacts et de nombreuses applications disponibles pour Android.

Côté serveur, l'implémentation de CardDAV est un serveur http qui répond aux méthodes http inhabituelles (PROPFIND, REPORT au lieu de GET, POST...). Heureusement il existe un module Go pour simplifier grandement le travail : github.com/emersion/go-webdav. Cette bibliothèque attend un Backend implémenté et fournit un http.Handler standard qui devrait servir les requêtes HTTP après authentification.

Authentification

Il est intéressant de noter que la bibliothèque ne fournit aucune aide concernant l'authentification des utilisateurs, mais grâce à la composabilité Go, ce n'est pas un problème.
CardDAV utilise les informations d'identification de base. Une fois les identifiants vérifiés, nous pouvons enregistrer ces identifiants dans le contexte (sera utile plus tard) :

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

Implémentation de l'interface CardDAV

La structure ownBackend doit implémenter l'interface carddav.Backend, qui n'est pas très fine, mais toujours gérable.

CurrentUserPrincipal et AddressBookHomeSetPath doivent fournir des URL (commençant et se terminant par une barre oblique). Il s'agira généralement du nom d'utilisateur/des contacts. C'est ici que vous devez extraire le nom d'utilisateur du contexte (qui est le seul argument disponible) :

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
}

Après cela, le plaisir peut commencer : vous devez implémenter les méthodes AddressBook, GetAddressObject et ListAddressObjects.

AddressBook renvoie une structure simple, où le chemin doit commencer par AddressBookHomeSetPath ci-dessus (et se terminer par une barre oblique)

GetAddressObject et ListAddressObjects doivent vérifier le chemin actuel (pour garantir que l'utilisateur actuellement authentifié peut accéder à ces contacts), puis renvoyer les contacts en tant qu'AddressObject.

AdresseObjet

L'AddressObject a plusieurs attributs, le plus important :

  • le chemin pour identifier ce contact particulier (peut être arbitraire, commencez par une barre oblique)
  • l'ETag pour permettre au client de vérifier rapidement si une mise à jour a eu lieu (si vous l'oubliez, iOS n'affichera rien)
  • la Carte qui attend une VCard

La VCard représente les données de contact réelles et doit probablement être adaptée en fonction de la façon dont vous stockez vos contacts. Dans mon cas, ça s'est terminé comme ça :

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
}

Prendre le raccourci en lecture seule

Certaines méthodes permettent de mettre à jour un contact. Comme je ne souhaite pas que ma liste de membres soit mise à jour via CardDAV, je renvoie une erreur 403 aux méthodes Put et Delete : return webdav.NewHTTPError(http.StatusForbidden, erreurs.New("carddav: opération non prise en charge"))

Tester localement

iOS nécessite que le serveur CardDAV serve via https. Vous pouvez générer des certificats auto-signés localement à l'aide d'openssl (remplacez 192.168.XXX.XXX par votre adresse IP) à insérer dans 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

Après cela, vous devriez pouvoir expérimenter localement en ajoutant un « compte de contact CardDAV » pointant vers votre propre adresse IP et votre propre port.

Conclusion

Implémenter un serveur CardDAV dans Go est un peu complexe, mais cela en vaut clairement la peine : vos contacts seront automatiquement synchronisés avec les données que vous avez sur le serveur de votre organisation !

Connaissez-vous d'autres protocoles sympas qui permettent ce type d'intégration native ? N'hésitez pas à partager vos expériences !

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn