”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 如何将通讯录与手机同步?在 Go 中实现 CardDAV!

如何将通讯录与手机同步?在 Go 中实现 CardDAV!

发布于2024-11-07
浏览:508

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
}

采用只读快捷方式

某些方法允许更新联系人。由于我不希望通过 CardDAV 更新我的成员列表,因此我向 Put 和 Delete 方法返回 403 错误: return webdav.NewHTTPError(http.StatusForbidden,errors.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

之后,您应该能够通过添加指向您自己的 IP 地址和端口的“CardDAV 联系帐户”来在本地进行实验。

结论

在 Go 中实现 CardDAV 服务器有点复杂,但显然值得:您的联系人将自动与您组织服务器上的数据同步!

您知道其他允许这种本机集成的很酷的协议吗?欢迎分享您的经验!

版本声明 本文转载于:https://dev.to/cmdscale/how-to-synchronize-your-contacts-with-your-phone-implemeting-carddav-in-go-9ia?1如有侵犯,请联系[email protected]删除
最新教程 更多>

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3