Add PoC CardDAV server
This commit is contained in:
parent
f7c8ce8d9f
commit
937242e3ef
|
@ -0,0 +1,88 @@
|
||||||
|
package carddav
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/hydroxide/protonmail"
|
||||||
|
"github.com/emersion/go-vcard"
|
||||||
|
"github.com/emersion/go-webdav/carddav"
|
||||||
|
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type addressObject struct {
|
||||||
|
c *protonmail.Client
|
||||||
|
contact *protonmail.ContactExport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ao *addressObject) ID() string {
|
||||||
|
return ao.contact.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ao *addressObject) Card() (vcard.Card, error) {
|
||||||
|
card := make(vcard.Card)
|
||||||
|
|
||||||
|
for _, c := range ao.contact.Cards {
|
||||||
|
if c.Type.Encrypted() {
|
||||||
|
// TODO: decrypt
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.Type.Signed() {
|
||||||
|
// TODO: check signature
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := vcard.NewDecoder(strings.NewReader(c.Data)).Decode()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, fields := range decoded {
|
||||||
|
for _, f := range fields {
|
||||||
|
card.Add(k, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return card, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type addressBook struct {
|
||||||
|
c *protonmail.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ab *addressBook) ListAddressObjects() ([]carddav.AddressObject, error) {
|
||||||
|
// TODO: cache this
|
||||||
|
// TODO: paging support
|
||||||
|
_, contacts, err := ab.c.ListContactsExport(0, 0)
|
||||||
|
log.Println(contacts, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
aos := make([]carddav.AddressObject, len(contacts))
|
||||||
|
for i, contact := range contacts {
|
||||||
|
aos[i] = &addressObject{c: ab.c, contact: contact}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ab *addressBook) GetAddressObject(id string) (carddav.AddressObject, error) {
|
||||||
|
contact, err := ab.c.GetContact(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &addressObject{
|
||||||
|
c: ab.c,
|
||||||
|
contact: &protonmail.ContactExport{
|
||||||
|
ID: contact.ID,
|
||||||
|
Cards: contact.Cards,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(c *protonmail.Client) http.Handler {
|
||||||
|
return carddav.NewHandler(&addressBook{c})
|
||||||
|
}
|
|
@ -5,8 +5,10 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/emersion/hydroxide/carddav"
|
||||||
"github.com/emersion/hydroxide/protonmail"
|
"github.com/emersion/hydroxide/protonmail"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -99,4 +101,14 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h := carddav.NewHandler(c)
|
||||||
|
|
||||||
|
s := &http.Server{
|
||||||
|
Addr: "127.0.0.1:8080",
|
||||||
|
Handler: h,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Starting server at", s.Addr)
|
||||||
|
log.Fatal(s.ListenAndServe())
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,14 @@ package protonmail
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/openpgp"
|
"golang.org/x/crypto/openpgp"
|
||||||
|
"golang.org/x/crypto/openpgp/armor"
|
||||||
|
"golang.org/x/crypto/openpgp/packet"
|
||||||
)
|
)
|
||||||
|
|
||||||
type authInfoReq struct {
|
type authInfoReq struct {
|
||||||
|
@ -204,6 +207,8 @@ func (c *Client) Unlock(auth *Auth, passphrase string) (openpgp.EntityList, erro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read private keys and unlock them
|
||||||
|
|
||||||
keyRing, err := openpgp.ReadArmoredKeyRing(strings.NewReader(auth.privateKey))
|
keyRing, err := openpgp.ReadArmoredKeyRing(strings.NewReader(auth.privateKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -213,13 +218,47 @@ func (c *Client) Unlock(auth *Auth, passphrase string) (openpgp.EntityList, erro
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, e := range keyRing {
|
for _, e := range keyRing {
|
||||||
if err := e.PrivateKey.Decrypt(passphraseBytes); err != nil {
|
var privateKeys []*packet.PrivateKey
|
||||||
return nil, err
|
|
||||||
|
// e.PrivateKey is a signing key
|
||||||
|
if e.PrivateKey != nil {
|
||||||
|
privateKeys = append(privateKeys, e.PrivateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.Subkeys are encryption keys
|
||||||
|
for _, subkey := range e.Subkeys {
|
||||||
|
if subkey.PrivateKey != nil {
|
||||||
|
privateKeys = append(privateKeys, subkey.PrivateKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, priv := range privateKeys {
|
||||||
|
if err := priv.Decrypt(passphraseBytes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decrypt access token
|
||||||
|
|
||||||
|
block, err := armor.Decode(strings.NewReader(auth.accessToken))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := openpgp.ReadMessage(block.Body, keyRing, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: maybe check signature
|
||||||
|
accessTokenBytes, err := ioutil.ReadAll(msg.UnverifiedBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
c.uid = auth.UID
|
c.uid = auth.UID
|
||||||
c.accessToken = auth.accessToken
|
c.accessToken = string(accessTokenBytes)
|
||||||
c.keyRing = keyRing
|
c.keyRing = keyRing
|
||||||
return keyRing, nil
|
return keyRing, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,37 +2,75 @@ package protonmail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Contact struct {
|
type Contact struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
|
UID string
|
||||||
|
Size int
|
||||||
|
CreateTime int
|
||||||
|
ModifyTime int
|
||||||
LabelIDs []string
|
LabelIDs []string
|
||||||
|
|
||||||
Emails []*ContactEmail
|
// Not when using ListContacts
|
||||||
Data []*ContactData
|
ContactEmails []*ContactEmail
|
||||||
|
Cards []*ContactCard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ContactEmailDefaults int
|
||||||
|
|
||||||
type ContactEmail struct {
|
type ContactEmail struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
|
||||||
Email string
|
Email string
|
||||||
Type string
|
Type []string
|
||||||
Encrypt int
|
Defaults ContactEmailDefaults
|
||||||
Order int
|
Order int
|
||||||
ContactID string
|
ContactID string
|
||||||
LabelIDs []string
|
LabelIDs []string
|
||||||
|
|
||||||
|
// Only when using ListContactsEmails
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContactDataType int
|
type ContactCardType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ContactDataEncrypted ContactDataType = 1
|
ContactCardCleartext ContactCardType = iota
|
||||||
|
ContactCardEncrypted
|
||||||
|
ContactCardSigned
|
||||||
|
ContactCardEncryptedAndSigned
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContactData struct {
|
func (t ContactCardType) Signed() bool {
|
||||||
Type ContactDataType
|
switch t {
|
||||||
Data string
|
case ContactCardSigned, ContactCardEncryptedAndSigned:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t ContactCardType) Encrypted() bool {
|
||||||
|
switch t {
|
||||||
|
case ContactCardEncrypted, ContactCardEncryptedAndSigned:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContactCard struct {
|
||||||
|
Type ContactCardType
|
||||||
|
Data string
|
||||||
|
Signature string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContactExport struct {
|
||||||
|
ID string
|
||||||
|
Cards []*ContactCard
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ListContacts() ([]*Contact, error) {
|
func (c *Client) ListContacts() ([]*Contact, error) {
|
||||||
|
@ -52,21 +90,52 @@ func (c *Client) ListContacts() ([]*Contact, error) {
|
||||||
return respData.Contacts, nil
|
return respData.Contacts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ListContactsEmails() ([]*ContactEmail, error) {
|
func (c *Client) ListContactsEmails(page, pageSize int) (total int, emails []*ContactEmail, err error) {
|
||||||
req, err := c.newRequest(http.MethodGet, "/contacts/emails", nil)
|
v := url.Values{}
|
||||||
|
v.Set("Page", strconv.Itoa(page))
|
||||||
|
if pageSize > 0 {
|
||||||
|
v.Set("PageSize", strconv.Itoa(pageSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := c.newRequest(http.MethodGet, "/contacts/emails?"+v.Encode(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var respData struct {
|
var respData struct {
|
||||||
resp
|
resp
|
||||||
Contacts []*ContactEmail
|
ContactEmails []*ContactEmail
|
||||||
|
Total int
|
||||||
}
|
}
|
||||||
if err := c.doJSON(req, &respData); err != nil {
|
if err := c.doJSON(req, &respData); err != nil {
|
||||||
return nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return respData.Contacts, nil
|
return respData.Total, respData.ContactEmails, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListContactsExport(page, pageSize int) (total int, contacts []*ContactExport, err error) {
|
||||||
|
v := url.Values{}
|
||||||
|
v.Set("Page", strconv.Itoa(page))
|
||||||
|
if pageSize > 0 {
|
||||||
|
v.Set("PageSize", strconv.Itoa(pageSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := c.newRequest(http.MethodGet, "/contacts/export?"+v.Encode(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var respData struct {
|
||||||
|
resp
|
||||||
|
Contacts []*ContactExport
|
||||||
|
Total int
|
||||||
|
}
|
||||||
|
if err := c.doJSON(req, &respData); err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return respData.Total, respData.Contacts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetContact(id string) (*Contact, error) {
|
func (c *Client) GetContact(id string) (*Contact, error) {
|
||||||
|
|
Loading…
Reference in New Issue