Compare commits

...

10 Commits

Author SHA1 Message Date
Conrad Hoffmann
4ca7d8c4e7 More depency upgrades 2024-04-19 17:40:46 +02:00
Conrad Hoffmann
832c91beba Update pam to v2.0.0 2024-04-19 17:37:55 +02:00
Conrad Hoffmann
adb2a8bdfb storage: adapt to go-webdav interface changes 2024-04-19 17:29:50 +02:00
Conrad Hoffmann
39f90686f9 Upgrade dependencies 2024-04-19 17:29:44 +02:00
Simon Ser
a95896216f Migrate to go-imap v2
v1 is no longer actively maintained.

Co-authored-by: Conrad Hoffmann <ch@bitfehler.net>
2024-04-19 15:23:37 +02:00
Simon Ser
ebb5aede92 Add OAuth 2.0 backend 2024-02-21 17:07:08 +01:00
Conrad Hoffmann
cca1d579db storage/filesytem: more consistent logging 2024-02-05 22:48:00 +01:00
Conrad Hoffmann
a115c50037 auth/*: more consistent logging 2024-02-05 22:24:17 +01:00
Conrad Hoffmann
96e33a0777 Add tokidoki(8) man page 2024-02-05 21:44:34 +01:00
Conrad Hoffmann
a87520cb0f Add htpasswd-style static file auth module
Can be used via `-auth.url=file://`. Only supports bcrypt password
hashes ($2y). Use e.g. `htpasswd -c -BC 14 <filename> <user>` to create
a file. Documentation forthcoming.
2024-02-05 17:23:11 +01:00
11 changed files with 561 additions and 153 deletions

91
auth/htpasswd.go Normal file
View File

@ -0,0 +1,91 @@
package auth
import (
"bufio"
"fmt"
"net/http"
"os"
"strings"
"golang.org/x/crypto/bcrypt"
"github.com/rs/zerolog/log"
)
// This provider provides htpasswd style authentication, but _only_ if the
// bcrypt algorithm is used (hash must start with $2y). Use e.g.
// `htpasswd -c -BC 17 <filename> <user>`
type htpasswdProvider struct {
users map[string]string
}
func NewHtpasswd(location string) (AuthProvider, error) {
file, err := os.Open(location)
if err != nil {
return nil, fmt.Errorf("failed to open %s: %s", location, err.Error())
}
defer file.Close()
var result htpasswdProvider
result.users = make(map[string]string)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fields := strings.Split(scanner.Text(), ":")
if len(fields) != 2 {
return nil, fmt.Errorf("failed to parse %s: %s: expected 2 fields, found %d", location, scanner.Text(), len(fields))
}
if !strings.HasPrefix(fields[1], "$2y$") {
return nil, fmt.Errorf("failed to parse %s: %s is not a bcrypt hash ($2y)", location, scanner.Text())
}
result.users[fields[0]] = fields[1]
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to parse %s: %s", location, err.Error())
}
return &result, nil
}
func (prov *htpasswdProvider) Middleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
prov.htpasswdAuth(next, w, r)
})
}
}
func (prov *htpasswdProvider) htpasswdAuth(next http.Handler, w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok {
w.Header().Add("WWW-Authenticate", `Basic realm="Please provide your system credentials", charset="UTF-8"`)
http.Error(w, "HTTP Basic auth is required", http.StatusUnauthorized)
return
}
hash, ok := prov.users[user]
if !ok {
log.Debug().Str("user", user).Msg("auth error")
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
return
}
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)); err != nil {
if err != bcrypt.ErrMismatchedHashAndPassword {
log.Warn().Err(err).Str("user", user).Msg("password check failed")
} else {
log.Debug().Str("user", user).Msg("auth error")
}
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
return
}
authCtx := AuthContext{
AuthMethod: "htpasswd",
UserName: user,
}
ctx := NewContext(r.Context(), &authCtx)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
}

View File

@ -5,8 +5,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-sasl"
"github.com/emersion/go-imap/v2/imapclient"
)
type IMAPProvider struct {
@ -44,15 +43,14 @@ func (prov *IMAPProvider) doAuth(next http.Handler,
conn, err := prov.dial()
if err != nil {
log.Debug().Err(err).Msg("auth dial error")
log.Warn().Err(err).Msg("auth dial error")
http.Error(w, "Temporary authentication error, try again later", http.StatusServiceUnavailable)
return
}
defer conn.Close()
auth := sasl.NewPlainClient("", user, pass)
if err := conn.Authenticate(auth); err != nil {
log.Debug().Err(err).Msg("auth error")
if err := conn.Login(user, pass).Wait(); err != nil {
log.Debug().Str("user", user).Err(err).Msg("auth error")
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
return
}
@ -67,10 +65,10 @@ func (prov *IMAPProvider) doAuth(next http.Handler,
next.ServeHTTP(w, r)
}
func (prov *IMAPProvider) dial() (*client.Client, error) {
func (prov *IMAPProvider) dial() (*imapclient.Client, error) {
if prov.tls {
return client.DialTLS(prov.addr, nil)
return imapclient.DialTLS(prov.addr, nil)
} else {
return client.Dial(prov.addr)
return imapclient.DialInsecure(prov.addr, nil)
}
}

87
auth/oauth2.go Normal file
View File

@ -0,0 +1,87 @@
package auth
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/rs/zerolog/log"
"git.sr.ht/~emersion/go-oauth2"
)
type OAuth2Provider struct {
metadata *oauth2.ServerMetadata
clientID string
clientSecret string
}
// Initializes a new OAuth 2.0 auth provider with the given connection string.
func NewOAuth2(endpoint, clientID, clientSecret string) (AuthProvider, error) {
metadata, err := oauth2.DiscoverServerMetadata(context.Background(), endpoint)
if err != nil {
return nil, fmt.Errorf("failed to fetch OAuth 2.0 server metadata: %v", err)
}
return &OAuth2Provider{
metadata: metadata,
clientID: clientID,
clientSecret: clientSecret,
}, nil
}
func (prov *OAuth2Provider) Middleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
prov.doAuth(next, w, r)
})
}
}
func (prov *OAuth2Provider) doAuth(next http.Handler,
w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
authScheme, creds, _ := strings.Cut(auth, " ")
var username, accessToken string
switch strings.ToLower(authScheme) {
case "bearer":
accessToken = creds
case "basic":
username, accessToken, _ = r.BasicAuth()
default:
w.Header().Add("WWW-Authenticate", `Bearer, Basic realm="Please provide an OAuth access token", charset="UTF-8"`)
http.Error(w, "HTTP auth is required", http.StatusUnauthorized)
return
}
client := oauth2.Client{
Server: prov.metadata,
ClientID: prov.clientID,
ClientSecret: prov.clientSecret,
}
resp, err := client.Introspect(r.Context(), accessToken)
if err != nil || !resp.Active {
log.Debug().Err(err).Msg("auth error")
http.Error(w, "Invalid access token", http.StatusUnauthorized)
return
}
if username != "" && username != resp.Username {
http.Error(w, "Invalid username", http.StatusUnauthorized)
return
}
if resp.Username == "" {
http.Error(w, "OAuth 2.0 server did not send username", http.StatusInternalServerError)
return
}
authCtx := AuthContext{
AuthMethod: "oauth2",
UserName: resp.Username,
}
ctx := NewContext(r.Context(), &authCtx)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
}

View File

@ -6,7 +6,7 @@ import (
"fmt"
"net/http"
"github.com/msteinert/pam"
"github.com/msteinert/pam/v2"
"github.com/rs/zerolog/log"
)
@ -44,26 +44,32 @@ func pamAuth(next http.Handler, w http.ResponseWriter, r *http.Request) {
}
})
if err != nil {
log.Debug().Err(err).Msg("failed to start PAM conversation")
log.Warn().Err(err).Msg("failed to start PAM conversation")
http.Error(w, "Temporary authentication error, try again later", http.StatusServiceUnavailable)
return
}
defer func() {
err := t.End()
if err != nil {
log.Warn().Err(err).Msg("failed to end PAM transaction")
}
}()
if err := t.Authenticate(0); err != nil {
log.Debug().Err(err).Msg("auth error")
log.Debug().Str("user", user).Err(err).Msg("auth error")
http.Error(w, "Invalid username or password", http.StatusUnauthorized)
return
}
if err := t.AcctMgmt(0); err != nil {
log.Debug().Err(err).Msg("account unavailable")
log.Debug().Str("user", user).Err(err).Msg("account unavailable")
http.Error(w, "Account unavailable", http.StatusUnauthorized)
return
}
user, err = t.GetItem(pam.User)
if err != nil {
log.Debug().Err(err).Msg("failed to get PAM username")
log.Warn().Str("user", user).Err(err).Msg("failed to get PAM username")
http.Error(w, "Temporary authentication error, try again later", http.StatusServiceUnavailable)
return
}

View File

@ -18,8 +18,22 @@ func NewFromURL(authURL string) (AuthProvider, error) {
return NewIMAP(u.Host, true), nil
case "pam":
return NewPAM()
case "file":
path := u.Path
if u.Host != "" {
path = u.Host + path
}
return NewHtpasswd(path)
case "null":
return NewNull()
case "http", "https":
if u.User == nil {
return nil, fmt.Errorf("missing client ID for OAuth 2.0")
}
clientID := u.User.Username()
clientSecret, _ := u.User.Password()
u.User = nil
return NewOAuth2(u.String(), clientID, clientSecret)
default:
return nil, fmt.Errorf("no auth provider found for %s:// URL", u.Scheme)
}

118
doc/tokidoki.8.scd Normal file
View File

@ -0,0 +1,118 @@
tokidoki(8)
# NAME
tokidoki - a CalDAV/CardDAV server
# SYNOPSIS
*tokidoki* [OPTIONS]
# DESCRIPTION
*tokidoki* runs a multi-user CalDAV/CardDAV server.
Currently, by default only a single address book and calendar per user are
supported, as users cannot create new ones. If additional resources are created
manually in the storage backend, *tokidoki* will however serve them just fine.
*tokidoki* supports calendar and address book auto-discovery via
*/.well-known/caldav* and */.well-known/carddav* respectively, as defined in RFC
6764, section 6. Hence, most clients should be able to discover available
resources by just pointing them at the server root.
Regular logs are sent to stderr, HTTP logs are sent to stdout.
# OPTIONS
*-addr* _addr_
Bind to the specified address/port. Default: ":8080" (port 8080 on all
interfaces.
*-auth.url* _url_
Auth backend URL (required). See AUTH BACKENDS below.
*-cert* _filename_
Enable TLS and load certificate from _filename_. Requires *-key*.
*-key* _filename_
Enable TLS and load key from _filename_. Requires *-cert*.
*-log.debug*
Enable debug logs.
*-log.json*
Enable structured logs.
# AUTH BACKENDS
Currently, all requests to tokidoki must be authenticated. To validate
usernames and passwords, tokidoki supports a number of auth backends. The
following authentication backends are available:
## IMAP
The IMAP auth backend defers authentication to the provided IMAP server.
Convenient for large-scale deployments in conjunction with email services, or
self-hosted email setups. Do not use this unless you control the specified IMAP
server. Deferring authentication to e.g. Gmail would allow any Gmail user to
authenticate.
URL: *imaps://*_server_*:*_port_
_Note:_ for development, *imap://* is also supported (plain IMAP without
encryption). This is not recommended for production use.
## PAM
The PAM auth backed defers authentication to the local *PAM*(8) subsystem. This
allows e.g. authentication as system user(s). Convenient for (mostly)
single-user self-hosted setups.
URL: *pam://* (no parameters)
_Note:_ The PAM auth backend must be enabled at build time, as PAM may not be
available on all platforms.
## OAuth 2.0
The OAuth 2.0 auth backend delegates authentication to the provided OAuth 2.0
server.
URL: *https://*_client_id_*:*_client_secret_*@*_host_
## Static file (htpasswd)
The static file auth backend relies on the file format popularized by Apache and
other web servers for basic authentication. Such files are often created and edited
using *htpasswd*(1). Convenient for small setups where virtual users (i.e. users
that are not system users) are required or desired.
URL: *file://*_path_ (both absolute and relative paths are supported)
_Note:_ This backend has the significant limitation that it only supports
bcrypt-hashed passwords (recognizable by the hash starting with *$2y$*). To
create a file, use e.g. the following command:
```
htpasswd -c -B -C 17 <filename> <user>
```
# STORAGE BACKENDS
To store users calendars and address books, tokidoki requires a storage backend.
Currently, the following storage backends are available:
## Filesystem
The filesystem storage backend stores every event and every contact as
individual file, in folders organized by user and resource, underneath the
provided base path. The filesystem backend is relatively simple, with good
performance.
URL: *file://*_path_ (absolute path required)
# SEE ALSO
Links to the source code and mailing lists for discussion and development of
*tokidoki* can be found at https://sr.ht/~sircmpwn/tokidoki.

20
go.mod
View File

@ -3,20 +3,22 @@ module git.sr.ht/~sircmpwn/tokidoki
go 1.18
require (
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f
github.com/emersion/go-imap v1.2.1
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
git.sr.ht/~emersion/go-oauth2 v0.0.0-20240217160856-2e0d6e20b088
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6
github.com/emersion/go-imap/v2 v2.0.0-beta.2.0.20240417100641-a587a14d3f01
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
github.com/emersion/go-webdav v0.5.1-0.20240202164822-eaac65215b3a
github.com/go-chi/chi/v5 v5.0.10
github.com/msteinert/pam v1.2.0
github.com/rs/zerolog v1.31.0
github.com/emersion/go-webdav v0.5.1-0.20240419143909-21f251fa1de2
github.com/go-chi/chi/v5 v5.0.12
github.com/msteinert/pam/v2 v2.0.0
github.com/rs/zerolog v1.32.0
golang.org/x/crypto v0.18.0
)
require (
github.com/emersion/go-message v0.18.1 // indirect
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/teambition/rrule-go v1.8.2 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sys v0.19.0 // indirect
)

70
go.sum
View File

@ -1,19 +1,20 @@
git.sr.ht/~emersion/go-oauth2 v0.0.0-20240217160856-2e0d6e20b088 h1:KuPliLD8CQM1WbCHdjHR6mhadIzLaAJCNENmvB1y9gs=
git.sr.ht/~emersion/go-oauth2 v0.0.0-20240217160856-2e0d6e20b088/go.mod h1:VHj0jSCLIkrfEwmOvJ4+ykpoVbD/YLN7BM523oKKBHc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f h1:feGUUxxvOtWVOhTko8Cbmp33a+tU0IMZxMEmnkoAISQ=
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ=
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6 h1:kHoSgklT8weIDl6R6xFpBJ5IioRdBU1v2X2aCZRVCcM=
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw=
github.com/emersion/go-imap/v2 v2.0.0-beta.2.0.20240417100641-a587a14d3f01 h1:dq/06hDbCT+/DpbKWSrfrTeiJW97ION78N6J6Mktp2w=
github.com/emersion/go-imap/v2 v2.0.0-beta.2.0.20240417100641-a587a14d3f01/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk=
github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E=
github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA=
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-webdav v0.5.1-0.20240202164822-eaac65215b3a h1:IHOPSOw+XXKRSkuXTOeEtDejkuXqf0ohjZq9hew6ArA=
github.com/emersion/go-webdav v0.5.1-0.20240202164822-eaac65215b3a/go.mod h1:ycyIzTelG5pHln4t+Y32/zBvmrM7+mV7x+V+Gx4ZQno=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/emersion/go-webdav v0.5.1-0.20240419143909-21f251fa1de2 h1:k/NO/RfeXFuKGcpHDkspYoE8u6tWoHs03tH5DXg22To=
github.com/emersion/go-webdav v0.5.1-0.20240419143909-21f251fa1de2/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@ -21,23 +22,50 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE=
github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0=
github.com/msteinert/pam/v2 v2.0.0 h1:jnObb8MT6jvMbmrUQO5J/puTUjxy7Av+55zVJRJsCyE=
github.com/msteinert/pam/v2 v2.0.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -189,19 +189,12 @@ func (b *filesystemBackend) ListCalendars(ctx context.Context) ([]caldav.Calenda
if err == nil && len(result) == 0 {
// Nothing here yet? Create the default calendar.
log.Debug().Msg("no calendars found, creating default calendar")
cal, err_ := b.createDefaultCalendar(ctx)
if err_ != nil {
log.Debug().Int("results", len(result)).Bool("success", false).Str("error", err_.Error()).Msg("filesystem.ListCalendars() done")
return nil, fmt.Errorf("error creating default calendar: %s", err_.Error())
cal, err := b.createDefaultCalendar(ctx)
if err == nil {
result = append(result, *cal)
}
result = append(result, *cal)
}
if err != nil {
log.Warn().Int("results", len(result)).Bool("success", false).Str("error", err.Error()).Msg("filesystem.ListCalendars() done")
} else {
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.ListCalendars() done")
}
log.Debug().Int("results", len(result)).Err(err).Msg("filesystem.ListCalendars() done")
return result, err
}
@ -214,14 +207,14 @@ func (b *filesystemBackend) GetCalendar(ctx context.Context, urlPath string) (*c
}
localPath = filepath.Join(localPath, calendarFileName)
log.Debug().Str("local_path", localPath).Msg("loading calendar")
log.Debug().Str("path", localPath).Msg("loading calendar")
data, readErr := os.ReadFile(localPath)
if readErr != nil {
if os.IsNotExist(readErr) {
data, err := os.ReadFile(localPath)
if err != nil {
if os.IsNotExist(err) {
return nil, webdav.NewHTTPError(404, err)
}
return nil, fmt.Errorf("error opening calendar: %s", readErr.Error())
return nil, fmt.Errorf("error opening calendar: %s", err.Error())
}
var calendar caldav.Calendar
err = json.Unmarshal(data, &calendar)
@ -232,8 +225,12 @@ func (b *filesystemBackend) GetCalendar(ctx context.Context, urlPath string) (*c
return &calendar, nil
}
func (b *filesystemBackend) CreateCalendar(ctx context.Context, calendar *caldav.Calendar) error {
panic("TODO")
}
func (b *filesystemBackend) GetCalendarObject(ctx context.Context, objPath string, req *caldav.CalendarCompRequest) (*caldav.CalendarObject, error) {
log.Debug().Str("url_path", objPath).Msg("filesystem.GetCalendarObject()")
log.Debug().Str("path", objPath).Msg("filesystem.GetCalendarObject()")
localPath, err := b.safeLocalCalDAVPath(ctx, objPath)
if err != nil {
@ -243,7 +240,6 @@ func (b *filesystemBackend) GetCalendarObject(ctx context.Context, objPath strin
info, err := os.Stat(localPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
log.Debug().Str("local_path", localPath).Msg("object not found")
return nil, webdav.NewHTTPError(404, err)
}
return nil, err
@ -256,7 +252,7 @@ func (b *filesystemBackend) GetCalendarObject(ctx context.Context, objPath strin
calendar, err := calendarFromFile(localPath, propFilter)
if err != nil {
log.Debug().Err(err).Msg("error reading calendar")
log.Debug().Str("path", localPath).Err(err).Msg("error reading calendar")
return nil, err
}
@ -284,11 +280,7 @@ func (b *filesystemBackend) ListCalendarObjects(ctx context.Context, urlPath str
}
result, err := b.loadAllCalendarObjects(ctx, urlPath, propFilter)
if err != nil {
log.Warn().Int("results", len(result)).Bool("success", false).Str("error", err.Error()).Msg("filesystem.ListCalendarObjects() done")
} else {
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.ListCalendarObjects() done")
}
log.Debug().Int("results", len(result)).Err(err).Msg("filesystem.ListCalendarObjects() done")
return result, err
}
@ -301,28 +293,22 @@ func (b *filesystemBackend) QueryCalendarObjects(ctx context.Context, urlPath st
}
result, err := b.loadAllCalendarObjects(ctx, urlPath, propFilter)
log.Debug().Int("results", len(result)).Err(err).Msg("filesystem.QueryCalendarObjects() load done")
if err != nil {
log.Warn().Int("results", len(result)).Str("error", err.Error()).Msg("filesystem.QueryCalendarObjects() error loading")
return result, err
}
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.QueryCalendarObjects() load done")
filtered, err := caldav.Filter(query, result)
if err != nil {
log.Warn().Int("results", len(result)).Str("error", err.Error()).Msg("filesystem.QueryCalendarObjects() error filtering")
return result, err
}
log.Debug().Int("results", len(filtered)).Bool("success", true).Msg("filesystem.QueryCalendarObjects() done")
return filtered, nil
log.Debug().Int("results", len(filtered)).Err(err).Msg("filesystem.QueryCalendarObjects() filter done")
return filtered, err
}
func (b *filesystemBackend) PutCalendarObject(ctx context.Context, objPath string, calendar *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (loc string, err error) {
log.Debug().Str("url_path", objPath).Msg("filesystem.PutCalendarObject()")
func (b *filesystemBackend) PutCalendarObject(ctx context.Context, objPath string, calendar *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (*caldav.CalendarObject, error) {
log.Debug().Str("path", objPath).Msg("filesystem.PutCalendarObject()")
_, uid, err := caldav.ValidateCalendarObject(calendar)
if err != nil {
return "", caldav.NewPreconditionError(caldav.PreconditionValidCalendarObjectResource)
return nil, caldav.NewPreconditionError(caldav.PreconditionValidCalendarObjectResource)
}
// Object always get saved as <UID>.ics
@ -331,7 +317,7 @@ func (b *filesystemBackend) PutCalendarObject(ctx context.Context, objPath strin
localPath, err := b.safeLocalCalDAVPath(ctx, objPath)
if err != nil {
return "", err
return nil, err
}
flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC
@ -346,37 +332,53 @@ func (b *filesystemBackend) PutCalendarObject(ctx context.Context, objPath strin
// Make sure we overwrite the _right_ file
etag, err := etagForFile(localPath)
if err != nil {
return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
return nil, webdav.NewHTTPError(http.StatusPreconditionFailed, err)
}
want, err := opts.IfMatch.ETag()
if err != nil {
return "", webdav.NewHTTPError(http.StatusBadRequest, err)
return nil, webdav.NewHTTPError(http.StatusBadRequest, err)
}
if want != etag {
err = fmt.Errorf("If-Match does not match current ETag (%s/%s)", want, etag)
return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
return nil, webdav.NewHTTPError(http.StatusPreconditionFailed, err)
}
}
f, err := os.OpenFile(localPath, flags, 0666)
if os.IsExist(err) {
return "", caldav.NewPreconditionError(caldav.PreconditionNoUIDConflict)
return nil, caldav.NewPreconditionError(caldav.PreconditionNoUIDConflict)
} else if err != nil {
return "", err
return nil, err
}
defer f.Close()
enc := ical.NewEncoder(f)
err = enc.Encode(calendar)
if err != nil {
return "", err
return nil, err
}
return objPath, nil
etag, err := etagForFile(localPath)
if err != nil {
return nil, err
}
info, err := f.Stat()
if err != nil {
return nil, err
}
r := caldav.CalendarObject{
Path: objPath,
ModTime: info.ModTime(),
ContentLength: info.Size(),
ETag: etag,
Data: calendar,
}
return &r, nil
}
func (b *filesystemBackend) DeleteCalendarObject(ctx context.Context, path string) error {
log.Debug().Str("url_path", path).Msg("filesystem.DeleteCalendarObject()")
log.Debug().Str("path", path).Msg("filesystem.DeleteCalendarObject()")
localPath, err := b.safeLocalCalDAVPath(ctx, path)
if err != nil {

View File

@ -129,22 +129,51 @@ func (b *filesystemBackend) loadAllAddressObjects(ctx context.Context, urlPath s
return result, err
}
func (b *filesystemBackend) createDefaultAddressBook(ctx context.Context) (*carddav.AddressBook, error) {
// TODO what should the default address book look like?
localPath, err_ := b.localCardDAVDir(ctx, defaultResourceName)
if err_ != nil {
return nil, fmt.Errorf("error creating default address book: %s", err_.Error())
func (b *filesystemBackend) writeAddressBook(ctx context.Context, ab *carddav.AddressBook) error {
localPath, err := b.safeLocalCardDAVPath(ctx, ab.Path)
if err != nil {
return err
}
homeSetPath, err_ := b.AddressBookHomeSetPath(ctx)
if err_ != nil {
return nil, fmt.Errorf("error creating default address book: %s", err_.Error())
log.Debug().Str("local", localPath).Str("url", ab.Path).Msg("filesystem.writeAddressBook()")
blob, err := json.MarshalIndent(ab, "", " ")
if err != nil {
return err
}
return os.WriteFile(path.Join(localPath, addressBookFileName), blob, 0644)
if err != nil {
return fmt.Errorf("error writing address book: %s", err.Error())
}
return nil
}
func (b *filesystemBackend) createAddressBook(ctx context.Context, ab *carddav.AddressBook) error {
localPath, err := b.safeLocalCardDAVPath(ctx, ab.Path)
if err != nil {
return err
}
log.Debug().Str("local", localPath).Str("url", ab.Path).Msg("filesystem.createAddressBook()")
err = os.Mkdir(localPath, 0755)
if err != nil {
return fmt.Errorf("error creating address book: %s", err.Error())
}
return b.writeAddressBook(ctx, ab)
}
func (b *filesystemBackend) createDefaultAddressBook(ctx context.Context) (*carddav.AddressBook, error) {
log.Debug().Msg("filesystem.createDefaultAddressBook()")
homeSetPath, err := b.AddressBookHomeSetPath(ctx)
if err != nil {
return nil, fmt.Errorf("error creating default address book: %s", err.Error())
}
urlPath := path.Join(homeSetPath, defaultResourceName) + "/"
log.Debug().Str("local", localPath).Str("url", urlPath).Msg("filesystem.createDefaultAddressBook()")
// TODO what should the default address book look like?
defaultAB := carddav.AddressBook{
Path: urlPath,
Name: "My contacts",
@ -152,14 +181,8 @@ func (b *filesystemBackend) createDefaultAddressBook(ctx context.Context) (*card
MaxResourceSize: 1024,
SupportedAddressData: nil,
}
blob, err := json.MarshalIndent(defaultAB, "", " ")
if err != nil {
return nil, fmt.Errorf("error creating default address book: %s", err.Error())
}
err = os.WriteFile(path.Join(localPath, addressBookFileName), blob, 0644)
if err != nil {
return nil, fmt.Errorf("error writing default address book: %s", err.Error())
}
err = b.createAddressBook(ctx, &defaultAB)
log.Debug().Err(err).Msg("filesystem.createDefaultAddressBook() done")
return &defaultAB, nil
}
@ -207,19 +230,12 @@ func (b *filesystemBackend) ListAddressBooks(ctx context.Context) ([]carddav.Add
if err == nil && len(result) == 0 {
// Nothing here yet? Create the default address book
log.Debug().Msg("no address books found, creating default address book")
ab, err_ := b.createDefaultAddressBook(ctx)
if err_ != nil {
log.Debug().Int("results", len(result)).Bool("success", false).Str("error", err_.Error()).Msg("filesystem.ListAddressBooks() done")
return nil, fmt.Errorf("error creating default address book: %s", err_.Error())
ab, err := b.createDefaultAddressBook(ctx)
if err == nil {
result = append(result, *ab)
}
result = append(result, *ab)
}
if err != nil {
log.Warn().Int("results", len(result)).Bool("success", false).Str("error", err.Error()).Msg("filesystem.ListAddressBooks() done")
} else {
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.ListAddressBooks() done")
}
log.Debug().Int("results", len(result)).Err(err).Msg("filesystem.ListAddressBooks() done")
return result, err
}
@ -232,15 +248,14 @@ func (b *filesystemBackend) GetAddressBook(ctx context.Context, urlPath string)
}
localPath = filepath.Join(localPath, addressBookFileName)
log.Debug().Str("local_path", localPath).Msg("loading addressbook")
log.Debug().Str("path", localPath).Msg("loading addressbook")
data, readErr := os.ReadFile(localPath)
if readErr != nil {
if os.IsNotExist(readErr) {
data, err := os.ReadFile(localPath)
if err != nil {
if os.IsNotExist(err) {
return nil, webdav.NewHTTPError(404, err)
}
return nil, fmt.Errorf("error opening address book: %s", readErr.Error())
return nil, fmt.Errorf("error opening address book: %s", err.Error())
}
var addressBook carddav.AddressBook
err = json.Unmarshal(data, &addressBook)
@ -251,8 +266,37 @@ func (b *filesystemBackend) GetAddressBook(ctx context.Context, urlPath string)
return &addressBook, nil
}
func (b *filesystemBackend) CreateAddressBook(ctx context.Context, ab *carddav.AddressBook) error {
log.Debug().Str("path", ab.Path).Msg("filesystem.CreateAddressBook()")
ab.MaxResourceSize = 4096
err := b.createAddressBook(ctx, ab)
log.Debug().Err(err).Msg("filesystem.CreateAddressBook() done")
return err
}
func (b *filesystemBackend) UpdateAddressBook(ctx context.Context, ab *carddav.AddressBook) error {
log.Debug().Str("path", ab.Path).Msg("filesystem.UpdateAddressBook()")
ab.MaxResourceSize = 4096
err := b.writeAddressBook(ctx, ab)
log.Debug().Err(err).Msg("filesystem.UpdateAddressBook() done")
return err
}
func (b *filesystemBackend) DeleteAddressBook(ctx context.Context, urlPath string) error {
log.Debug().Str("path", urlPath).Msg("filesystem.DeleteAddressBook()")
localPath, err := b.safeLocalCardDAVPath(ctx, urlPath)
if err != nil {
return err
}
log.Debug().Str("path", localPath).Msg("deleting addressbook")
err = os.RemoveAll(localPath)
log.Debug().Err(err).Msg("filesystem.DeleteAddressBook() done")
return err
}
func (b *filesystemBackend) GetAddressObject(ctx context.Context, objPath string, req *carddav.AddressDataRequest) (*carddav.AddressObject, error) {
log.Debug().Str("url_path", objPath).Msg("filesystem.GetAddressObject()")
log.Debug().Str("path", objPath).Msg("filesystem.GetAddressObject()")
localPath, err := b.safeLocalCardDAVPath(ctx, objPath)
if err != nil {
@ -274,6 +318,7 @@ func (b *filesystemBackend) GetAddressObject(ctx context.Context, objPath string
card, err := vcardFromFile(localPath, propFilter)
if err != nil {
log.Debug().Str("path", localPath).Err(err).Msg("error reading calendar")
return nil, err
}
@ -301,11 +346,7 @@ func (b *filesystemBackend) ListAddressObjects(ctx context.Context, urlPath stri
}
result, err := b.loadAllAddressObjects(ctx, urlPath, propFilter)
if err != nil {
log.Warn().Int("results", len(result)).Bool("success", false).Str("error", err.Error()).Msg("filesystem.ListAddressObjects() done")
} else {
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.ListAddressObjects() done")
}
log.Debug().Int("results", len(result)).Err(err).Msg("filesystem.ListAddressObjects() done")
return result, err
}
@ -318,24 +359,18 @@ func (b *filesystemBackend) QueryAddressObjects(ctx context.Context, urlPath str
}
result, err := b.loadAllAddressObjects(ctx, urlPath, propFilter)
log.Debug().Int("results", len(result)).Err(err).Msg("filesystem.QueryAddressObjects() load done")
if err != nil {
log.Warn().Int("results", len(result)).Str("error", err.Error()).Msg("filesystem.QueryAddressObjects() error loading")
return result, err
}
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.QueryAddressObjects() load done")
filtered, err := carddav.Filter(query, result)
if err != nil {
log.Warn().Int("results", len(result)).Str("error", err.Error()).Msg("filesystem.QueryAddressObjects() error filtering")
return result, err
}
log.Debug().Int("results", len(filtered)).Bool("success", true).Msg("filesystem.QueryAddressObjects() done")
return filtered, nil
log.Debug().Int("results", len(filtered)).Err(err).Msg("filesystem.QueryAddressObjects() filter done")
return filtered, err
}
func (b *filesystemBackend) PutAddressObject(ctx context.Context, objPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (loc string, err error) {
log.Debug().Str("url_path", objPath).Msg("filesystem.PutAddressObject()")
func (b *filesystemBackend) PutAddressObject(ctx context.Context, objPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (*carddav.AddressObject, error) {
log.Debug().Str("path", objPath).Msg("filesystem.PutAddressObject()")
// Object always get saved as <UID>.vcf
dirname, _ := path.Split(objPath)
@ -343,7 +378,7 @@ func (b *filesystemBackend) PutAddressObject(ctx context.Context, objPath string
localPath, err := b.safeLocalCardDAVPath(ctx, objPath)
if err != nil {
return "", err
return nil, err
}
flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC
@ -358,37 +393,52 @@ func (b *filesystemBackend) PutAddressObject(ctx context.Context, objPath string
// Make sure we overwrite the _right_ file
etag, err := etagForFile(localPath)
if err != nil {
return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
return nil, webdav.NewHTTPError(http.StatusPreconditionFailed, err)
}
want, err := opts.IfMatch.ETag()
if err != nil {
return "", webdav.NewHTTPError(http.StatusBadRequest, err)
return nil, webdav.NewHTTPError(http.StatusBadRequest, err)
}
if want != etag {
err = fmt.Errorf("If-Match does not match current ETag (%s/%s)", want, etag)
return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
return nil, webdav.NewHTTPError(http.StatusPreconditionFailed, err)
}
}
f, err := os.OpenFile(localPath, flags, 0666)
if os.IsExist(err) {
return "", carddav.NewPreconditionError(carddav.PreconditionNoUIDConflict)
return nil, carddav.NewPreconditionError(carddav.PreconditionNoUIDConflict)
} else if err != nil {
return "", err
return nil, err
}
defer f.Close()
enc := vcard.NewEncoder(f)
err = enc.Encode(card)
if err != nil {
return "", err
if err := enc.Encode(card); err != nil {
return nil, err
}
return objPath, nil
etag, err := etagForFile(localPath)
if err != nil {
return nil, err
}
info, err := f.Stat()
if err != nil {
return nil, err
}
r := carddav.AddressObject{
Path: objPath,
ModTime: info.ModTime(),
ContentLength: info.Size(),
ETag: etag,
Card: card,
}
return &r, nil
}
func (b *filesystemBackend) DeleteAddressObject(ctx context.Context, path string) error {
log.Debug().Str("url_path", path).Msg("filesystem.DeleteAddressObject()")
log.Debug().Str("path", path).Msg("filesystem.DeleteAddressObject()")
localPath, err := b.safeLocalCardDAVPath(ctx, path)
if err != nil {

View File

@ -32,6 +32,18 @@ func (*psqlBackend) GetAddressBook(ctx context.Context, path string) (*carddav.A
panic("TODO")
}
func (*psqlBackend) CreateAddressBook(ctx context.Context, ab *carddav.AddressBook) error {
panic("TODO")
}
func (*psqlBackend) UpdateAddressBook(ctx context.Context, ab *carddav.AddressBook) error {
panic("TODO")
}
func (*psqlBackend) DeleteAddressBook(ctx context.Context, path string) error {
panic("TODO")
}
func (*psqlBackend) GetAddressObject(ctx context.Context, path string, req *carddav.AddressDataRequest) (*carddav.AddressObject, error) {
panic("TODO")
}
@ -44,7 +56,7 @@ func (*psqlBackend) QueryAddressObjects(ctx context.Context, path string, query
panic("TODO")
}
func (*psqlBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (loc string, err error) {
func (*psqlBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (*carddav.AddressObject, error) {
panic("TODO")
}