diff --git a/kernel/go.mod b/kernel/go.mod index 40a50d2d4..84fb8243d 100644 --- a/kernel/go.mod +++ b/kernel/go.mod @@ -23,6 +23,8 @@ require ( github.com/denisbrodbeck/machineid v1.0.1 github.com/dgraph-io/ristretto v1.0.0 github.com/djherbis/times v1.6.0 + github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff + github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135 github.com/emirpasic/gods v1.18.1 github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb github.com/flopp/go-findfont v0.1.0 diff --git a/kernel/go.sum b/kernel/go.sum index bef35047e..a28f63693 100644 --- a/kernel/go.sum +++ b/kernel/go.sum @@ -89,6 +89,12 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw= +github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= +github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg= +github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= +github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135 h1:Ssk00uh7jhctJ23eclGxhhGqplSQB+wCt6fmbjhnOS8= +github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= @@ -367,6 +373,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU= github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= +github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4= github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw= github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go new file mode 100644 index 000000000..8b2aad21a --- /dev/null +++ b/kernel/model/carddav.go @@ -0,0 +1,880 @@ +// SiYuan - Refactor your thinking +// Copyright (c) 2020-present, b3log.org +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package model + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "strings" + "sync" + + "github.com/88250/gulu" + "github.com/emersion/go-vcard" + "github.com/emersion/go-webdav/carddav" + "github.com/siyuan-note/logging" + "github.com/siyuan-note/siyuan/kernel/util" +) + +const ( + // REF: https://developers.google.com/people/carddav#resources + CardDavPrefixPath = "/carddav" + CardDavPrincipalsPath = CardDavPrefixPath + "/principals" // 0 resourceTypeRoot + CardDavUserPrincipalPath = CardDavPrincipalsPath + "/main" // 1 resourceTypeUserPrincipal + CardDavHomeSetPath = CardDavUserPrincipalPath + "/contacts" // 2 resourceTypeAddressBookHomeSet + + CardDavDefaultAddressBookPath = CardDavHomeSetPath + "/default" // 3 resourceTypeAddressBook + CardDavDefaultAddressBookName = "default" + + CardDavAddressBooksMetaDataFilePath = CardDavHomeSetPath + "/address-books.json" +) + +type PathDepth int + +const ( + pathDepth_Root PathDepth = 1 + iota // /carddav + pathDepth_Principals // /carddav/principals + pathDepth_UserPrincipal // /carddav/principals/main + pathDepth_HomeSet // /carddav/principals/main/contacts + pathDepth_AddressBook // /carddav/principals/main/contacts/default + pathDepth_Address // /carddav/principals/main/contacts/default/id.vcf +) + +var ( + defaultAddressBook = carddav.AddressBook{ + Path: CardDavDefaultAddressBookPath, + Name: CardDavDefaultAddressBookName, + Description: "Default address book", + MaxResourceSize: 0, + } + contacts = Contacts{ + loaded: false, + changed: false, + lock: sync.Mutex{}, + books: sync.Map{}, + booksMetaData: []*carddav.AddressBook{}, + } + + ErrorNotFound = errors.New("CardDAV: not found") + ErrorPathInvalid = errors.New("CardDAV: path is invalid") + + ErrorBookNotFound = errors.New("CardDAV: address book not found") + ErrorBookPathInvalid = errors.New("CardDAV: address book path is invalid") + + ErrorAddressNotFound = errors.New("CardDAV: address not found") + ErrorAddressFileExtensionNameInvalid = errors.New("CardDAV: address file extension name is invalid") +) + +// CardDavPath2DirectoryPath converts CardDAV path to absolute path of the file system +func CardDavPath2DirectoryPath(cardDavPath string) string { + return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(cardDavPath, "/")) +} + +// HomeSetPathPath returns the absolute path of the address book home set directory +func HomeSetPathPath() string { + return CardDavPath2DirectoryPath(CardDavHomeSetPath) +} + +// AddressBooksMetaDataFilePath returns the absolute path of the address books meta data file +func AddressBooksMetaDataFilePath() string { + return CardDavPath2DirectoryPath(CardDavAddressBooksMetaDataFilePath) +} + +func GetPathDepth(urlPath string) PathDepth { + urlPath = path.Clean(urlPath) + return PathDepth(len(strings.Split(urlPath, "/")) - 1) +} + +// ParseAddressPath parses address path to address book path and address ID +func ParseAddressPath(addressPath string) (addressBookPath string, addressID string, err error) { + addressBookPath, addressFileName := path.Split(addressPath) + addressID = path.Base(addressFileName) + addressFileExt := path.Ext(addressFileName) + + if GetPathDepth(addressBookPath) != pathDepth_AddressBook { + err = ErrorBookPathInvalid + return + } + + if addressFileExt != ".vcf" { + err = ErrorAddressFileExtensionNameInvalid + return + } + + return +} + +// AddressPropsFilter filters address properties +func AddressPropsFilter(address *carddav.AddressObject, req *carddav.AddressDataRequest) *carddav.AddressObject { + var card *vcard.Card + card = &address.Card + + // if req.AllProp { + // card = &address.Card + // } else { + // card = &vcard.Card{} + // for _, prop := range req.Props { + // fields := address.Card[prop] + // if fields != nil { + // for _, field := range fields { + // card.Add(prop, field) + // } + // } + // } + // } + + return &carddav.AddressObject{ + Path: address.Path, + ModTime: address.ModTime, + ContentLength: address.ContentLength, + ETag: address.ETag, + Card: *card, + } +} + +func LoadCards(filePath string) (cards []*vcard.Card, err error) { + data, err := os.ReadFile(filePath) + if err != nil { + logging.LogErrorf("read vCard file [%s] failed: %s", filePath, err) + return + } + + decoder := vcard.NewDecoder(bytes.NewReader(data)) + for { + card, err := decoder.Decode() + if err != nil { + if err == io.EOF { + break + } + logging.LogErrorf("decode vCard file [%s] failed: %s", filePath, err) + return nil, err + } + cards = append(cards, &card) + } + + return +} + +type Contacts struct { + loaded bool + changed bool + lock sync.Mutex // load & save + books sync.Map // Path -> *AddressBook + booksMetaData []*carddav.AddressBook +} + +// load all contacts +func (c *Contacts) load() error { + c.books.Clear() + addressBooksMetaDataFilePath := AddressBooksMetaDataFilePath() + metaData, err := os.ReadFile(addressBooksMetaDataFilePath) + if os.IsNotExist(err) { + c.booksMetaData = []*carddav.AddressBook{&defaultAddressBook} + if err := c.saveAddressBooksMetaData(); err != nil { + return err + } + } else { + // load meta data file + c.booksMetaData = []*carddav.AddressBook{} + if err = gulu.JSON.UnmarshalJSON(metaData, &c.booksMetaData); err != nil { + logging.LogErrorf("unmarshal address books meta data failed: %s", err) + return err + } + } + + wg := &sync.WaitGroup{} + wg.Add(len(c.booksMetaData)) + for _, addressBookMetaData := range c.booksMetaData { + addressBook := &AddressBook{ + Changed: false, + DirectoryPath: CardDavPath2DirectoryPath(addressBookMetaData.Path), + MetaData: addressBookMetaData, + Addresses: sync.Map{}, + } + c.books.Store(addressBookMetaData.Path, addressBook) + go func() { + defer wg.Done() + addressBook.load() + }() + } + wg.Wait() + + c.loaded = true + c.changed = false + return nil +} + +// save all contacts +func (c *Contacts) save(force bool) error { + if force || c.changed { + // save address books meta data + if err := c.saveAddressBooksMetaData(); err != nil { + return err + } + + // save all address to *.vbf files + wg := &sync.WaitGroup{} + c.books.Range(func(path any, book any) bool { + wg.Add(1) + go func() { + defer wg.Done() + // path_ := path.(string) + book_ := book.(*AddressBook) + book_.save(force) + }() + return true + }) + wg.Wait() + c.changed = false + } + return nil +} + +// save all contacts +func (c *Contacts) saveAddressBooksMetaData() error { + data, err := gulu.JSON.MarshalIndentJSON(c.booksMetaData, "", " ") + if err != nil { + logging.LogErrorf("marshal address books meta data failed: %s", err) + return err + } + + dirPath := HomeSetPathPath() + if err := os.MkdirAll(dirPath, 0755); err != nil { + logging.LogErrorf("create directory [%s] failed: %s", dirPath, err) + return err + } + + filePath := AddressBooksMetaDataFilePath() + if err := os.WriteFile(filePath, data, 0755); err != nil { + logging.LogErrorf("write file [%s] failed: %s", filePath, err) + return err + } + + return nil +} + +func (c *Contacts) Load() error { + c.lock.Lock() + defer c.lock.Unlock() + + if !c.loaded { + return c.load() + } + return nil +} + +func (c *Contacts) GetAddress(addressPath string) (addressBook *AddressBook, addressObject *AddressObject, err error) { + bookPath, addressID, err := ParseAddressPath(addressPath) + if err != nil { + logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err) + return + } + + if value, ok := c.books.Load(bookPath); ok { + addressBook = value.(*AddressBook) + } else { + err = ErrorBookNotFound + return + } + + if value, ok := addressBook.Addresses.Load(addressID); ok { + addressObject = value.(*AddressObject) + } else { + err = ErrorAddressNotFound + return + } + + return +} + +func (c *Contacts) ListAddressBooks() (addressBooks []carddav.AddressBook, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + for _, addressBook := range contacts.booksMetaData { + addressBooks = append(addressBooks, *addressBook) + } + return +} + +func (c *Contacts) GetAddressBook(path string) (addressBook *carddav.AddressBook, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + if book, ok := contacts.books.Load(path); ok { + addressBook = book.(*AddressBook).MetaData + return + } + + err = ErrorBookNotFound + return +} + +func (c *Contacts) CreateAddressBook(addressBookMetaData *carddav.AddressBook) (err error) { + c.lock.Lock() + defer c.lock.Unlock() + + var addressBook *AddressBook + + // update map + if value, ok := c.books.Load(addressBookMetaData.Path); ok { + // update map item + addressBook = value.(*AddressBook) + addressBook.MetaData = addressBookMetaData + } else { + // insert map item + addressBook = &AddressBook{ + Changed: false, + DirectoryPath: CardDavPath2DirectoryPath(addressBookMetaData.Path), + MetaData: addressBookMetaData, + Addresses: sync.Map{}, + } + c.books.Store(addressBookMetaData.Path, addressBook) + } + + var index = -1 + for i, item := range c.booksMetaData { + if item.Path == addressBookMetaData.Path { + index = i + break + } + } + + if index >= 0 { + // update list + c.booksMetaData[index] = addressBookMetaData + } else { + // insert list + c.booksMetaData = append(c.booksMetaData, addressBookMetaData) + } + + // create address book directory + if err = os.MkdirAll(addressBook.DirectoryPath, 0755); err != nil { + logging.LogErrorf("create directory [%s] failed: %s", addressBook, err) + return + } + + // save meta data + if err = c.saveAddressBooksMetaData(); err != nil { + return + } + + return +} + +func (c *Contacts) DeleteAddressBook(path string) (err error) { + c.lock.Lock() + defer c.lock.Unlock() + + var addressBook *AddressBook + + // delete map item + if value, loaded := c.books.LoadAndDelete(path); loaded { + addressBook = value.(*AddressBook) + } + + // delete list item + for i, item := range c.booksMetaData { + if item.Path == path { + c.booksMetaData = append(c.booksMetaData[:i], c.booksMetaData[i+1:]...) + break + } + } + + // remove address book directory + if err = os.RemoveAll(addressBook.DirectoryPath); err != nil { + logging.LogErrorf("remove directory [%s] failed: %s", addressBook, err) + return + } + + // save meta data + if err = c.saveAddressBooksMetaData(); err != nil { + return + } + + return nil +} + +func (c *Contacts) GetAddressObject(addressPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + _, address, err := c.GetAddress(addressPath) + if err != nil { + return + } + + addressObject = AddressPropsFilter(address.Data, req) + return +} + +func (c *Contacts) ListAddressObjects(bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + var addressBook *AddressBook + if value, ok := c.books.Load(bookPath); ok { + addressBook = value.(*AddressBook) + } else { + err = ErrorBookNotFound + return + } + + addressBook.Addresses.Range(func(id any, address any) bool { + addressObjects = append(addressObjects, *AddressPropsFilter(address.(*AddressObject).Data, req)) + return true + }) + + return +} + +func (c *Contacts) QueryAddressObjects(urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + switch GetPathDepth(urlPath) { + case pathDepth_Root, pathDepth_Principals, pathDepth_UserPrincipal, pathDepth_HomeSet: + c.books.Range(func(path any, book any) bool { + addressBook := book.(*AddressBook) + addressBook.Addresses.Range(func(id any, address any) bool { + addressObjects = append(addressObjects, *address.(*AddressObject).Data) + return true + }) + return true + }) + case pathDepth_AddressBook: + if value, ok := c.books.Load(urlPath); ok { + addressBook := value.(*AddressBook) + addressBook.Addresses.Range(func(id any, address any) bool { + addressObjects = append(addressObjects, *address.(*AddressObject).Data) + return true + }) + } + case pathDepth_Address: + if _, address, _ := c.GetAddress(urlPath); address != nil { + addressObjects = append(addressObjects, *address.Data) + } + default: + err = ErrorPathInvalid + return + } + + addressObjects, err = carddav.Filter(query, addressObjects) + return +} + +func (c *Contacts) PutAddressObject(addressPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + bookPath, addressID, err := ParseAddressPath(addressPath) + if err != nil { + logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err) + return + } + + var addressBook *AddressBook + if value, ok := c.books.Load(bookPath); ok { + addressBook = value.(*AddressBook) + } else { + err = ErrorBookNotFound + return + } + + var address *AddressObject + if value, ok := addressBook.Addresses.Load(addressID); ok { + address = value.(*AddressObject) + + if opts.IfNoneMatch.IsSet() { + addressObject = address.Data + return + } + + address.Data.Card = card + address.Changed = true + } else { + address = &AddressObject{ + Changed: true, + FilePath: CardDavPath2DirectoryPath(addressPath), + BookPath: bookPath, + Data: &carddav.AddressObject{ + Card: card, + }, + } + } + + err = address.save(true) + if err != nil { + return + } + + err = address.update() + if err != nil { + return + } + + addressBook.Addresses.Store(addressID, address) + addressObject = address.Data + return +} + +func (c *Contacts) DeleteAddressObject(addressPath string) (err error) { + c.lock.Lock() + defer c.lock.Unlock() + + _, address, err := c.GetAddress(addressPath) + if err != nil && err != ErrorAddressNotFound { + return + } + + if err = os.Remove(address.FilePath); err != nil { + logging.LogErrorf("remove file [%s] failed: %s", address.FilePath, err) + return + } + + return +} + +type AddressBook struct { + Changed bool + DirectoryPath string + MetaData *carddav.AddressBook + Addresses sync.Map // id -> *AddressObject +} + +// load an address book from multiple *.vcf files +func (b *AddressBook) load() error { + if err := os.MkdirAll(b.DirectoryPath, 0755); err != nil { + logging.LogErrorf("create directory [%s] failed: %s", b.DirectoryPath, err) + return err + } + + entries, err := os.ReadDir(b.DirectoryPath) + if err != nil { + logging.LogErrorf("read dir [%s] failed: %s", b.DirectoryPath, err) + return err + } + + wg := &sync.WaitGroup{} + for _, entry := range entries { + if !entry.IsDir() { + filename := entry.Name() + ext := path.Ext(filename) + if ext == ".vcf" { + wg.Add(1) + go func() { + defer wg.Done() + + // load cards + addressFilePath := path.Join(b.DirectoryPath, filename) + vCards, err := LoadCards(addressFilePath) + if err != nil { + return + } + + switch len(vCards) { + case 0: // invalid file + case 1: // file contain 1 card + address := &AddressObject{ + FilePath: addressFilePath, + BookPath: b.MetaData.Path, + Data: &carddav.AddressObject{ + Card: *vCards[0], + }, + } + if err := address.update(); err != nil { + return + } + + id := path.Base(filename) + b.Addresses.Store(id, address) + default: // file contain multiple cards + // Create a file for each card + addressesWaitGroup := &sync.WaitGroup{} + for _, vCard := range vCards { + addressesWaitGroup.Add(1) + go func() { + defer addressesWaitGroup.Done() + filename_ := util.AssetName(filename) + address := &AddressObject{ + FilePath: path.Join(b.DirectoryPath, filename_), + BookPath: b.MetaData.Path, + Data: &carddav.AddressObject{ + Card: *vCard, + }, + } + if err := address.save(true); err != nil { + return + } + if err := address.update(); err != nil { + return + } + + id := path.Base(filename) + b.Addresses.Store(id, address) + }() + } + + addressesWaitGroup.Wait() + // Delete original file with multiple cards + if err := os.Remove(addressFilePath); err != nil { + logging.LogErrorf("remove file [%s] failed: %s", addressFilePath, err) + return + } + } + }() + } + } + } + wg.Wait() + return nil +} + +// save an address book to multiple *.vcf files +func (b *AddressBook) save(force bool) error { + if force || b.Changed { + // create directory + if err := os.MkdirAll(b.DirectoryPath, 0755); err != nil { + logging.LogErrorf("create directory [%s] failed: %s", b.DirectoryPath, err) + return err + } + + wg := &sync.WaitGroup{} + b.Addresses.Range(func(id any, address any) bool { + wg.Add(1) + go func() { + defer wg.Done() + // id_ := id.(string) + address_ := address.(*AddressObject) + address_.save(force) + address_.update() + }() + return true + }) + wg.Wait() + b.Changed = false + } + + return nil +} + +type AddressObject struct { + Changed bool + FilePath string + BookPath string + Data *carddav.AddressObject +} + +// load an address from *.vcf file +func (o *AddressObject) load() error { + // get file info + addressFileInfo, err := os.Stat(o.FilePath) + if err != nil { + logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err) + return err + } + + // read file + addressData, err := os.ReadFile(o.FilePath) + if err != nil { + logging.LogErrorf("read file [%s] failed: %s", o.FilePath, err) + return err + } + + // decode file + reader := bytes.NewReader(addressData) + decoder := vcard.NewDecoder(reader) + card, err := decoder.Decode() + if err != nil { + logging.LogErrorf("decode file [%s] failed: %s", o.FilePath, err) + return err + } + + // load data + o.Changed = false + o.Data = &carddav.AddressObject{ + Path: path.Join(o.BookPath, addressFileInfo.Name()), + ModTime: addressFileInfo.ModTime(), + ContentLength: addressFileInfo.Size(), + ETag: fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()), + Card: card, + } + return nil +} + +// save an address to *.vcf file +func (o *AddressObject) save(force bool) error { + if force || o.Changed { + var addressData bytes.Buffer + + // encode data + encoder := vcard.NewEncoder(&addressData) + if err := encoder.Encode(o.Data.Card); err != nil { + logging.LogErrorf("encode card [%s] failed: %s", o.Data.Path, err) + return err + } + + // create directory + dirPath := path.Dir(o.FilePath) + if err := os.MkdirAll(dirPath, 0755); err != nil { + logging.LogErrorf("create directory [%s] failed: %s", dirPath, err) + return err + } + + // write file + if err := os.WriteFile(o.FilePath, addressData.Bytes(), 0755); err != nil { + logging.LogErrorf("write file [%s] failed: %s", o.FilePath, err) + return err + } + + o.Changed = false + } + return nil +} + +// update file info +func (o *AddressObject) update() error { + // update file info + addressFileInfo, err := os.Stat(o.FilePath) + if err != nil { + logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err) + return err + } + + o.Data.Path = path.Join(o.BookPath, addressFileInfo.Name()) + o.Data.ModTime = addressFileInfo.ModTime() + o.Data.ContentLength = addressFileInfo.Size() + o.Data.ETag = fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()) + + return nil +} + +type CardDavBackend struct{} + +func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) { + // logging.LogDebugf("CardDAV CurrentUserPrincipal") + return CardDavUserPrincipalPath, nil +} + +func (b *CardDavBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) { + // logging.LogDebugf("CardDAV AddressBookHomeSetPath") + return CardDavHomeSetPath, nil +} + +func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []carddav.AddressBook, err error) { + // logging.LogDebugf("CardDAV ListAddressBooks") + if err = contacts.Load(); err != nil { + return + } + + addressBooks, err = contacts.ListAddressBooks() + // logging.LogDebugf("CardDAV ListAddressBooks <- addressBooks: %#v, err: %s", addressBooks, err) + return +} + +func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (addressBook *carddav.AddressBook, err error) { + // logging.LogDebugf("CardDAV GetAddressBook -> bookPath: %s", bookPath) + if err = contacts.Load(); err != nil { + return + } + + addressBook, err = contacts.GetAddressBook(bookPath) + // logging.LogDebugf("CardDAV GetAddressBook <- addressBook: %#v, err: %s", addressBook, err) + return +} + +func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) { + // logging.LogDebugf("CardDAV CreateAddressBook -> addressBook: %#v", addressBook) + if err = contacts.Load(); err != nil { + return + } + + err = contacts.CreateAddressBook(addressBook) + // logging.LogDebugf("CardDAV CreateAddressBook <- err: %s", err) + return +} + +func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string) (err error) { + // logging.LogDebugf("CardDAV DeleteAddressBook -> bookPath: %s", bookPath) + if err = contacts.Load(); err != nil { + return + } + + err = contacts.DeleteAddressBook(bookPath) + // logging.LogDebugf("CardDAV DeleteAddressBook <- err: %s", err) + return +} + +func (b *CardDavBackend) GetAddressObject(ctx context.Context, addressPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) { + // logging.LogDebugf("CardDAV GetAddressObject -> addressPath: %s, req: %#v", addressPath, req) + if err = contacts.Load(); err != nil { + return + } + + addressObject, err = contacts.GetAddressObject(addressPath, req) + // logging.LogDebugf("CardDAV GetAddressObject <- addressObject: %#v, err: %s", addressObject, err) + return +} + +func (b *CardDavBackend) ListAddressObjects(ctx context.Context, bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) { + // logging.LogDebugf("CardDAV ListAddressObjects -> bookPath: %s, req: %#v", bookPath, req) + if err = contacts.Load(); err != nil { + return + } + + addressObjects, err = contacts.ListAddressObjects(bookPath, req) + // logging.LogDebugf("CardDAV ListAddressObjects <- addressObjects: %#v, err: %s", addressObjects, err) + return +} + +func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { + // logging.LogDebugf("CardDAV QueryAddressObjects -> urlPath: %s, query: %#v", urlPath, query) + if err = contacts.Load(); err != nil { + return + } + + addressObjects, err = contacts.QueryAddressObjects(urlPath, query) + // logging.LogDebugf("CardDAV QueryAddressObjects <- addressObjects: %#v, err: %s", addressObjects, err) + return +} + +func (b *CardDavBackend) PutAddressObject(ctx context.Context, addressPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) { + // logging.LogDebugf("CardDAV PutAddressObject -> addressPath: %s, card: %#v, opts: %#v", addressPath, card, opts) + if err = contacts.Load(); err != nil { + return + } + + addressObject, err = contacts.PutAddressObject(addressPath, card, opts) + // logging.LogDebugf("CardDAV PutAddressObject <- addressObject: %#v, err: %s", addressObject, err) + return +} + +func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, addressPath string) (err error) { + // logging.LogDebugf("CardDAV DeleteAddressObject -> addressPath: %s", addressPath) + if err = contacts.Load(); err != nil { + return + } + + err = contacts.DeleteAddressObject(addressPath) + // logging.LogDebugf("CardDAV DeleteAddressObject <- err: %s", err) + return +} diff --git a/kernel/model/session.go b/kernel/model/session.go index ef0d42440..a86d3268a 100644 --- a/kernel/model/session.go +++ b/kernel/model/session.go @@ -34,6 +34,11 @@ import ( "github.com/steambap/captcha" ) +var ( + BasicAuthHeaderKey = "WWW-Authenticate" + BasicAuthHeaderValue = "Basic realm=\"SiYuan Authorization Require\", charset=\"UTF-8\"" +) + func LogoutAuth(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret) @@ -300,8 +305,8 @@ func CheckAuth(c *gin.Context) { } // WebDAV BasicAuth Authenticate - if strings.HasPrefix(c.Request.RequestURI, "/webdav") { - c.Header("WWW-Authenticate", "Basic realm=Authorization Required") + if strings.HasPrefix(c.Request.RequestURI, "/webdav") || strings.HasPrefix(c.Request.RequestURI, "/carddav") { + c.Header(BasicAuthHeaderKey, BasicAuthHeaderValue) c.AbortWithStatus(http.StatusUnauthorized) return } diff --git a/kernel/server/proxy/publish.go b/kernel/server/proxy/publish.go index 52506270e..65d9a7666 100644 --- a/kernel/server/proxy/publish.go +++ b/kernel/server/proxy/publish.go @@ -21,7 +21,6 @@ import ( "net" "net/http" "net/http/httputil" - "strconv" "github.com/siyuan-note/logging" "github.com/siyuan-note/siyuan/kernel/model" @@ -143,7 +142,7 @@ func (PublishServiceTransport) RoundTrip(request *http.Request) (response *http. ProtoMinor: request.ProtoMinor, Request: request, Header: http.Header{ - "WWW-Authenticate": {"Basic realm=" + strconv.Quote("Authorization Required")}, + model.BasicAuthHeaderKey: {model.BasicAuthHeaderValue}, }, Close: false, ContentLength: -1, diff --git a/kernel/server/serve.go b/kernel/server/serve.go index 4a42d1691..3d1bb676f 100644 --- a/kernel/server/serve.go +++ b/kernel/server/serve.go @@ -32,6 +32,7 @@ import ( "time" "github.com/88250/gulu" + "github.com/emersion/go-webdav/carddav" "github.com/gin-contrib/gzip" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" @@ -47,17 +48,63 @@ import ( "golang.org/x/net/webdav" ) +const ( + MethodMkcol = "MKCOL" + MethodCopy = "COPY" + MethodMove = "MOVE" + MethodLock = "LOCK" + MethodUnlock = "UNLOCK" + MethodPropFind = "PROPFIND" + MethodPropPatch = "PROPPATCH" + MethodReport = "REPORT" +) + var ( - cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf")) - WebDavMethod = []string{ - "OPTIONS", - "GET", "HEAD", - "POST", "PUT", - "DELETE", - "MKCOL", - "COPY", "MOVE", - "LOCK", "UNLOCK", - "PROPFIND", "PROPPATCH", + cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf")) + HttpMethods = []string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + http.MethodConnect, + http.MethodOptions, + http.MethodTrace, + } + WebDavMethods = []string{ + http.MethodOptions, + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodDelete, + + MethodMkcol, + MethodCopy, + MethodMove, + MethodLock, + MethodUnlock, + MethodPropFind, + MethodPropPatch, + } + CardDavMethods = []string{ + http.MethodOptions, + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodDelete, + + MethodMkcol, + // MethodCopy, + // MethodMove, + // MethodLock, + // MethodUnlock, + MethodPropFind, + MethodPropPatch, + + MethodReport, } ) @@ -88,6 +135,7 @@ func Serve(fastMode bool) { serveAppearance(ginServer) serveWebSocket(ginServer) serveWebDAV(ginServer) + serveCardDAV(ginServer) serveExport(ginServer) serveWidgets(ginServer) servePlugins(ginServer) @@ -616,10 +664,19 @@ func serveWebDAV(ginServer *gin.Engine) { } ginGroup := ginServer.Group("/webdav", model.CheckAuth, model.CheckAdminRole) - ginGroup.Match(WebDavMethod, "/*path", func(c *gin.Context) { + // ginGroup.Any NOT support extension methods (PROPFIND etc.) + ginGroup.Match(WebDavMethods, "/*path", func(c *gin.Context) { if util.ReadOnly { switch c.Request.Method { - case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH": + case http.MethodPost, + http.MethodPut, + http.MethodDelete, + MethodMkcol, + MethodCopy, + MethodMove, + MethodLock, + MethodUnlock, + MethodPropPatch: c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34))) return } @@ -628,6 +685,40 @@ func serveWebDAV(ginServer *gin.Engine) { }) } +func serveCardDAV(ginServer *gin.Engine) { + // REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go + handler := carddav.Handler{ + Backend: &model.CardDavBackend{}, + Prefix: model.CardDavPrincipalsPath, + } + + ginServer.Match(CardDavMethods, "/.well-known/carddav", func(c *gin.Context) { + handler.ServeHTTP(c.Writer, c.Request) + }) + + ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole) + ginGroup.Match(CardDavMethods, "/*path", func(c *gin.Context) { + // logging.LogDebugf("CardDAV -> [%s] %s", c.Request.Method, c.Request.URL.String()) + if util.ReadOnly { + switch c.Request.Method { + case http.MethodPost, + http.MethodPut, + http.MethodDelete, + MethodMkcol, + MethodCopy, + MethodMove, + MethodLock, + MethodUnlock, + MethodPropPatch: + c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34))) + return + } + } + handler.ServeHTTP(c.Writer, c.Request) + // logging.LogDebugf("CardDAV <- [%s] %v", c.Request.Method, c.Writer.Status()) + }) +} + func shortReqMsg(msg []byte) []byte { s := gulu.Str.FromBytes(msg) max := 128 @@ -644,15 +735,32 @@ func shortReqMsg(msg []byte) []byte { } func corsMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { + allowMethods := strings.Join(HttpMethods, ", ") + allowWebDavMethods := strings.Join(WebDavMethods, ", ") + allowCardDavMethods := strings.Join(CardDavMethods, ", ") + return func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Credentials", "true") c.Header("Access-Control-Allow-Headers", "origin, Content-Length, Content-Type, Authorization") - c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS") c.Header("Access-Control-Allow-Private-Network", "true") - if c.Request.Method == "OPTIONS" { + if strings.HasPrefix(c.Request.RequestURI, "/webdav/") { + c.Header("Access-Control-Allow-Methods", allowWebDavMethods) + c.Next() + return + } + + if strings.HasPrefix(c.Request.RequestURI, "/carddav/") { + c.Header("Access-Control-Allow-Methods", allowCardDavMethods) + c.Next() + return + } + + c.Header("Access-Control-Allow-Methods", allowMethods) + + switch c.Request.Method { + case http.MethodOptions: c.Header("Access-Control-Max-Age", "600") c.AbortWithStatus(204) return