Compare commits

..

No commits in common. "4ca7d8c4e7cab42a3c4caa90a9b025d1947cfe0c" and "536f83fa6144b2f64919206ef08bcb56663c10f8" have entirely different histories.

11 changed files with 153 additions and 561 deletions

View File

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

View File

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

View File

@ -18,22 +18,8 @@ func NewFromURL(authURL string) (AuthProvider, error) {
return NewIMAP(u.Host, true), nil return NewIMAP(u.Host, true), nil
case "pam": case "pam":
return NewPAM() return NewPAM()
case "file":
path := u.Path
if u.Host != "" {
path = u.Host + path
}
return NewHtpasswd(path)
case "null": case "null":
return NewNull() 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: default:
return nil, fmt.Errorf("no auth provider found for %s:// URL", u.Scheme) return nil, fmt.Errorf("no auth provider found for %s:// URL", u.Scheme)
} }

View File

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

70
go.sum
View File

@ -1,20 +1,19 @@
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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6 h1:kHoSgklT8weIDl6R6xFpBJ5IioRdBU1v2X2aCZRVCcM= github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f h1:feGUUxxvOtWVOhTko8Cbmp33a+tU0IMZxMEmnkoAISQ=
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw= github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ=
github.com/emersion/go-imap/v2 v2.0.0-beta.2.0.20240417100641-a587a14d3f01 h1:dq/06hDbCT+/DpbKWSrfrTeiJW97ION78N6J6Mktp2w= github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
github.com/emersion/go-imap/v2 v2.0.0-beta.2.0.20240417100641-a587a14d3f01/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk= github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY= 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-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 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA=
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-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-webdav v0.5.1-0.20240419143909-21f251fa1de2 h1:k/NO/RfeXFuKGcpHDkspYoE8u6tWoHs03tH5DXg22To= github.com/emersion/go-webdav v0.5.1-0.20240202164822-eaac65215b3a h1:IHOPSOw+XXKRSkuXTOeEtDejkuXqf0ohjZq9hew6ArA=
github.com/emersion/go-webdav v0.5.1-0.20240419143909-21f251fa1de2/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ= 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.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@ -22,50 +21,23 @@ 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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/msteinert/pam/v2 v2.0.0 h1:jnObb8MT6jvMbmrUQO5J/puTUjxy7Av+55zVJRJsCyE= github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE=
github.com/msteinert/pam/v2 v2.0.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc= github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 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/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8= 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/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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.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.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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-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,12 +189,19 @@ func (b *filesystemBackend) ListCalendars(ctx context.Context) ([]caldav.Calenda
if err == nil && len(result) == 0 { if err == nil && len(result) == 0 {
// Nothing here yet? Create the default calendar. // Nothing here yet? Create the default calendar.
log.Debug().Msg("no calendars found, creating default calendar") log.Debug().Msg("no calendars found, creating default calendar")
cal, err := b.createDefaultCalendar(ctx) cal, err_ := b.createDefaultCalendar(ctx)
if err == nil { 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())
}
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 return result, err
} }
@ -207,14 +214,14 @@ func (b *filesystemBackend) GetCalendar(ctx context.Context, urlPath string) (*c
} }
localPath = filepath.Join(localPath, calendarFileName) localPath = filepath.Join(localPath, calendarFileName)
log.Debug().Str("path", localPath).Msg("loading calendar") log.Debug().Str("local_path", localPath).Msg("loading calendar")
data, err := os.ReadFile(localPath) data, readErr := os.ReadFile(localPath)
if err != nil { if readErr != nil {
if os.IsNotExist(err) { if os.IsNotExist(readErr) {
return nil, webdav.NewHTTPError(404, err) return nil, webdav.NewHTTPError(404, err)
} }
return nil, fmt.Errorf("error opening calendar: %s", err.Error()) return nil, fmt.Errorf("error opening calendar: %s", readErr.Error())
} }
var calendar caldav.Calendar var calendar caldav.Calendar
err = json.Unmarshal(data, &calendar) err = json.Unmarshal(data, &calendar)
@ -225,12 +232,8 @@ func (b *filesystemBackend) GetCalendar(ctx context.Context, urlPath string) (*c
return &calendar, nil 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) { func (b *filesystemBackend) GetCalendarObject(ctx context.Context, objPath string, req *caldav.CalendarCompRequest) (*caldav.CalendarObject, error) {
log.Debug().Str("path", objPath).Msg("filesystem.GetCalendarObject()") log.Debug().Str("url_path", objPath).Msg("filesystem.GetCalendarObject()")
localPath, err := b.safeLocalCalDAVPath(ctx, objPath) localPath, err := b.safeLocalCalDAVPath(ctx, objPath)
if err != nil { if err != nil {
@ -240,6 +243,7 @@ func (b *filesystemBackend) GetCalendarObject(ctx context.Context, objPath strin
info, err := os.Stat(localPath) info, err := os.Stat(localPath)
if err != nil { if err != nil {
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
log.Debug().Str("local_path", localPath).Msg("object not found")
return nil, webdav.NewHTTPError(404, err) return nil, webdav.NewHTTPError(404, err)
} }
return nil, err return nil, err
@ -252,7 +256,7 @@ func (b *filesystemBackend) GetCalendarObject(ctx context.Context, objPath strin
calendar, err := calendarFromFile(localPath, propFilter) calendar, err := calendarFromFile(localPath, propFilter)
if err != nil { if err != nil {
log.Debug().Str("path", localPath).Err(err).Msg("error reading calendar") log.Debug().Err(err).Msg("error reading calendar")
return nil, err return nil, err
} }
@ -280,7 +284,11 @@ func (b *filesystemBackend) ListCalendarObjects(ctx context.Context, urlPath str
} }
result, err := b.loadAllCalendarObjects(ctx, urlPath, propFilter) result, err := b.loadAllCalendarObjects(ctx, urlPath, propFilter)
log.Debug().Int("results", len(result)).Err(err).Msg("filesystem.ListCalendarObjects() done") 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")
}
return result, err return result, err
} }
@ -293,22 +301,28 @@ func (b *filesystemBackend) QueryCalendarObjects(ctx context.Context, urlPath st
} }
result, err := b.loadAllCalendarObjects(ctx, urlPath, propFilter) result, err := b.loadAllCalendarObjects(ctx, urlPath, propFilter)
log.Debug().Int("results", len(result)).Err(err).Msg("filesystem.QueryCalendarObjects() load done")
if err != nil { if err != nil {
log.Warn().Int("results", len(result)).Str("error", err.Error()).Msg("filesystem.QueryCalendarObjects() error loading")
return result, err return result, err
} }
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.QueryCalendarObjects() load done")
filtered, err := caldav.Filter(query, result) filtered, err := caldav.Filter(query, result)
log.Debug().Int("results", len(filtered)).Err(err).Msg("filesystem.QueryCalendarObjects() filter done") if err != nil {
return filtered, err 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
} }
func (b *filesystemBackend) PutCalendarObject(ctx context.Context, objPath string, calendar *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (*caldav.CalendarObject, error) { func (b *filesystemBackend) PutCalendarObject(ctx context.Context, objPath string, calendar *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (loc string, err error) {
log.Debug().Str("path", objPath).Msg("filesystem.PutCalendarObject()") log.Debug().Str("url_path", objPath).Msg("filesystem.PutCalendarObject()")
_, uid, err := caldav.ValidateCalendarObject(calendar) _, uid, err := caldav.ValidateCalendarObject(calendar)
if err != nil { if err != nil {
return nil, caldav.NewPreconditionError(caldav.PreconditionValidCalendarObjectResource) return "", caldav.NewPreconditionError(caldav.PreconditionValidCalendarObjectResource)
} }
// Object always get saved as <UID>.ics // Object always get saved as <UID>.ics
@ -317,7 +331,7 @@ func (b *filesystemBackend) PutCalendarObject(ctx context.Context, objPath strin
localPath, err := b.safeLocalCalDAVPath(ctx, objPath) localPath, err := b.safeLocalCalDAVPath(ctx, objPath)
if err != nil { if err != nil {
return nil, err return "", err
} }
flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC
@ -332,53 +346,37 @@ func (b *filesystemBackend) PutCalendarObject(ctx context.Context, objPath strin
// Make sure we overwrite the _right_ file // Make sure we overwrite the _right_ file
etag, err := etagForFile(localPath) etag, err := etagForFile(localPath)
if err != nil { if err != nil {
return nil, webdav.NewHTTPError(http.StatusPreconditionFailed, err) return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
} }
want, err := opts.IfMatch.ETag() want, err := opts.IfMatch.ETag()
if err != nil { if err != nil {
return nil, webdav.NewHTTPError(http.StatusBadRequest, err) return "", webdav.NewHTTPError(http.StatusBadRequest, err)
} }
if want != etag { if want != etag {
err = fmt.Errorf("If-Match does not match current ETag (%s/%s)", want, etag) err = fmt.Errorf("If-Match does not match current ETag (%s/%s)", want, etag)
return nil, webdav.NewHTTPError(http.StatusPreconditionFailed, err) return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
} }
} }
f, err := os.OpenFile(localPath, flags, 0666) f, err := os.OpenFile(localPath, flags, 0666)
if os.IsExist(err) { if os.IsExist(err) {
return nil, caldav.NewPreconditionError(caldav.PreconditionNoUIDConflict) return "", caldav.NewPreconditionError(caldav.PreconditionNoUIDConflict)
} else if err != nil { } else if err != nil {
return nil, err return "", err
} }
defer f.Close() defer f.Close()
enc := ical.NewEncoder(f) enc := ical.NewEncoder(f)
err = enc.Encode(calendar) err = enc.Encode(calendar)
if err != nil { if err != nil {
return nil, err return "", err
} }
etag, err := etagForFile(localPath) return objPath, nil
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 { func (b *filesystemBackend) DeleteCalendarObject(ctx context.Context, path string) error {
log.Debug().Str("path", path).Msg("filesystem.DeleteCalendarObject()") log.Debug().Str("url_path", path).Msg("filesystem.DeleteCalendarObject()")
localPath, err := b.safeLocalCalDAVPath(ctx, path) localPath, err := b.safeLocalCalDAVPath(ctx, path)
if err != nil { if err != nil {

View File

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

View File

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