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"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/emersion/hydroxide/carddav"
|
||||
"github.com/emersion/hydroxide/protonmail"
|
||||
)
|
||||
|
||||
|
@ -99,4 +101,14 @@ func main() {
|
|||
if err != nil {
|
||||
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 (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
"golang.org/x/crypto/openpgp/armor"
|
||||
"golang.org/x/crypto/openpgp/packet"
|
||||
)
|
||||
|
||||
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))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -213,13 +218,47 @@ func (c *Client) Unlock(auth *Auth, passphrase string) (openpgp.EntityList, erro
|
|||
}
|
||||
|
||||
for _, e := range keyRing {
|
||||
if err := e.PrivateKey.Decrypt(passphraseBytes); err != nil {
|
||||
return nil, err
|
||||
var privateKeys []*packet.PrivateKey
|
||||
|
||||
// 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.accessToken = auth.accessToken
|
||||
c.accessToken = string(accessTokenBytes)
|
||||
c.keyRing = keyRing
|
||||
return keyRing, nil
|
||||
}
|
||||
|
|
|
@ -2,37 +2,75 @@ package protonmail
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Contact struct {
|
||||
ID string
|
||||
Name string
|
||||
UID string
|
||||
Size int
|
||||
CreateTime int
|
||||
ModifyTime int
|
||||
LabelIDs []string
|
||||
|
||||
Emails []*ContactEmail
|
||||
Data []*ContactData
|
||||
// Not when using ListContacts
|
||||
ContactEmails []*ContactEmail
|
||||
Cards []*ContactCard
|
||||
}
|
||||
|
||||
type ContactEmailDefaults int
|
||||
|
||||
type ContactEmail struct {
|
||||
ID string
|
||||
Name string
|
||||
Email string
|
||||
Type string
|
||||
Encrypt int
|
||||
Type []string
|
||||
Defaults ContactEmailDefaults
|
||||
Order int
|
||||
ContactID string
|
||||
LabelIDs []string
|
||||
|
||||
// Only when using ListContactsEmails
|
||||
Name string
|
||||
}
|
||||
|
||||
type ContactDataType int
|
||||
type ContactCardType int
|
||||
|
||||
const (
|
||||
ContactDataEncrypted ContactDataType = 1
|
||||
ContactCardCleartext ContactCardType = iota
|
||||
ContactCardEncrypted
|
||||
ContactCardSigned
|
||||
ContactCardEncryptedAndSigned
|
||||
)
|
||||
|
||||
type ContactData struct {
|
||||
Type ContactDataType
|
||||
Data string
|
||||
func (t ContactCardType) Signed() bool {
|
||||
switch t {
|
||||
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) {
|
||||
|
@ -52,21 +90,52 @@ func (c *Client) ListContacts() ([]*Contact, error) {
|
|||
return respData.Contacts, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListContactsEmails() ([]*ContactEmail, error) {
|
||||
req, err := c.newRequest(http.MethodGet, "/contacts/emails", nil)
|
||||
func (c *Client) ListContactsEmails(page, pageSize int) (total int, emails []*ContactEmail, 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/emails?"+v.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
var respData struct {
|
||||
resp
|
||||
Contacts []*ContactEmail
|
||||
ContactEmails []*ContactEmail
|
||||
Total int
|
||||
}
|
||||
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) {
|
||||
|
|
Loading…
Reference in New Issue