«Если рабочий хочет хорошо выполнять свою работу, он должен сначала заточить свои инструменты» — Конфуций, «Аналитики Конфуция. Лу Лингун»
титульная страница > программирование > Как синхронизировать контакты с телефоном? Реализация CardDAV в Go!

Как синхронизировать контакты с телефоном? Реализация CardDAV в Go!

Опубликовано 7 ноября 2024 г.
Просматривать:912

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

Предположим, вы помогаете управлять небольшой организацией или клубом и у вас есть база данных, в которой хранятся все данные об участниках (имена, телефоны, адреса электронной почты...).
Разве не было бы здорово иметь доступ к этой актуальной информации везде, где она вам нужна? Что ж, с CardDAV это возможно!

CardDAV — это хорошо поддерживаемый открытый стандарт управления контактами; он имеет встроенную интеграцию с приложением «Контакты» iOS и многими приложениями, доступными для Android.

Серверная реализация CardDAV представляет собой http-сервер, который реагирует на необычные http-методы (PROPFIND, REPORT вместо GET, POST...). К счастью, существует модуль Go, который значительно упрощает работу: github.com/emersion/go-webdav. Эта библиотека ожидает реализованный бэкэнд и предоставляет стандартный http.Handler, который должен обслуживать HTTP-запросы после аутентификации.

Аутентификация

Интересно, что библиотека не предоставляет никакой помощи по аутентификации пользователей, однако благодаря возможности компоновки Go это не проблема.
CardDAV использует учетные данные базовой аутентификации. После проверки учетных данных мы можем сохранить их в контексте (пригодится позже):

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

Реализация интерфейса CardDAV

Структура ownBackend должна реализовывать интерфейс carddav.Backend, который не очень тонкий, но все же управляемый.

CurrentUserPrincipal и AddressBookHomeSetPath должны предоставлять URL-адреса (начинающиеся и заканчивающиеся косой чертой). Обычно это имя пользователя/контакты. Здесь вам нужно извлечь имя пользователя из контекста (это единственный доступный аргумент):

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
}

После этого может начаться самое интересное: вам нужно реализовать методы AddressBook, GetAddressObject и ListAddressObjects.

AddressBook возвращает простую структуру, где путь должен начинаться с указанного выше AddressBookHomeSetPath (и заканчиваться косой чертой)

GetAddressObject и ListAddressObjects должны проверить текущий путь (чтобы гарантировать, что текущий аутентифицированный пользователь может получить доступ к этим контактам), а затем вернуть контакты как AddressObject.

АдресОбъект

Объект AddressObject имеет несколько атрибутов, наиболее важный из которых:

  • путь для идентификации данного конкретного контакта (может быть произвольным, начинается со слэша)
  • тег ETag, позволяющий клиенту быстро проверить, произошло ли какое-либо обновление (если вы его забудете, iOS ничего не покажет)
  • Карта, которая ожидает VCard

Карта VCard представляет собой фактические контактные данные и, вероятно, должна быть адаптирована в зависимости от того, как вы храните свои контакты. В моем случае это закончилось так:

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
}

Использование ярлыка Readonly

Некоторые методы позволяют обновить контакт. Поскольку я не хочу, чтобы мой список участников обновлялся через CardDAV, я возвращаю ошибку 403 методам Put и Delete: return webdav.NewHTTPError(http.StatusForbidden, error.New("carddav: операция не поддерживается"))

Тестирование локально

iOS требует, чтобы сервер CardDAV работал через https. Вы можете создавать самозаверяющие сертификаты локально с помощью openssl (замените 192.168.XXX.XXX на свой IP-адрес) для передачи в 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

После этого вы сможете поэкспериментировать локально, добавив «контактную учетную запись CardDAV», указывающую на ваш собственный IP-адрес и порт.

Заключение

Реализация сервера CardDAV в Go немного сложна, но оно того явно стоит: ваши контакты будут автоматически синхронизироваться с данными, имеющимися на сервере вашей организации!

Знаете ли вы другие интересные протоколы, которые позволяют реализовать такую ​​встроенную интеграцию? Не стесняйтесь поделиться своим опытом!

Заявление о выпуске Эта статья воспроизведена по адресу: https://dev.to/cmdscale/how-to-synchronize-your-contacts-with-your-phone-implemeting-carddav-in-go-9ia?1 Если есть какие-либо нарушения, пожалуйста, свяжитесь с Study_golang@163 .comdelete
Последний учебник Более>

Изучайте китайский

Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.

Copyright© 2022 湘ICP备2022001581号-3