首頁 >後端開發 >Golang >如何將通訊錄與手機同步?在 Go 中實現 CardDAV!

如何將通訊錄與手機同步?在 Go 中實現 CardDAV!

DDD
DDD原創
2024-09-19 22:21:191237瀏覽

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 伺服器有點複雜,但顯然值得:您的聯絡人將自動與您組織伺服器上的資料同步!

您知道其他允許這種本機整合的很酷的協定嗎?歡迎分享您的經驗!

以上是如何將通訊錄與手機同步?在 Go 中實現 CardDAV!的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn