Compare commits
10 Commits
536f83fa61
...
4ca7d8c4e7
Author | SHA1 | Date | |
---|---|---|---|
|
4ca7d8c4e7 | ||
|
832c91beba | ||
|
adb2a8bdfb | ||
|
39f90686f9 | ||
|
a95896216f | ||
|
ebb5aede92 | ||
|
cca1d579db | ||
|
a115c50037 | ||
|
96e33a0777 | ||
|
a87520cb0f |
91
auth/htpasswd.go
Normal file
91
auth/htpasswd.go
Normal 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)
|
||||
}
|
16
auth/imap.go
16
auth/imap.go
@ -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
87
auth/oauth2.go
Normal 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)
|
||||
}
|
16
auth/pam.go
16
auth/pam.go
@ -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
|
||||
}
|
||||
|
14
auth/url.go
14
auth/url.go
@ -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
118
doc/tokidoki.8.scd
Normal 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
20
go.mod
@ -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
70
go.sum
@ -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=
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user