Предположим, вы помогаете управлять небольшой организацией или клубом и у вас есть база данных, в которой хранятся все данные об участниках (имена, телефоны, адреса электронной почты...).
Разве не было бы здорово иметь доступ к этой актуальной информации везде, где она вам нужна? Что ж, с 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)) }) }
Структура 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 имеет несколько атрибутов, наиболее важный из которых:
Карта 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 }
Некоторые методы позволяют обновить контакт. Поскольку я не хочу, чтобы мой список участников обновлялся через 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 немного сложна, но оно того явно стоит: ваши контакты будут автоматически синхронизироваться с данными, имеющимися на сервере вашей организации!
Знаете ли вы другие интересные протоколы, которые позволяют реализовать такую встроенную интеграцию? Не стесняйтесь поделиться своим опытом!
Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.
Copyright© 2022 湘ICP备2022001581号-3