mirror of
https://git.sr.ht/~sircmpwn/tokidoki
synced 2025-12-12 06:07:22 +01:00
217 lines
5.5 KiB
Go
217 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"flag"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/emersion/go-webdav"
|
|
"github.com/emersion/go-webdav/caldav"
|
|
"github.com/emersion/go-webdav/carddav"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/rs/zerolog"
|
|
"github.com/rs/zerolog/log"
|
|
|
|
"git.sr.ht/~sircmpwn/tokidoki/auth"
|
|
"git.sr.ht/~sircmpwn/tokidoki/storage"
|
|
)
|
|
|
|
type userPrincipalBackend struct{}
|
|
|
|
func (u *userPrincipalBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
|
|
authCtx, ok := auth.FromContext(ctx)
|
|
if !ok {
|
|
panic("Invalid data in auth context!")
|
|
}
|
|
if authCtx == nil {
|
|
return "", fmt.Errorf("unauthenticated requests are not supported")
|
|
}
|
|
|
|
userDir := base64.RawStdEncoding.EncodeToString([]byte(authCtx.UserName))
|
|
return "/" + userDir + "/", nil
|
|
}
|
|
|
|
type tokidokiHandler struct {
|
|
upBackend webdav.UserPrincipalBackend
|
|
authBackend auth.AuthProvider
|
|
caldavBackend caldav.Backend
|
|
carddavBackend carddav.Backend
|
|
}
|
|
|
|
func (u *tokidokiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
userPrincipalPath, err := u.upBackend.CurrentUserPrincipal(r.Context())
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
}
|
|
|
|
var homeSets []webdav.BackendSuppliedHomeSet
|
|
if u.caldavBackend != nil {
|
|
path, err := u.caldavBackend.CalendarHomeSetPath(r.Context())
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
} else {
|
|
homeSets = append(homeSets, caldav.NewCalendarHomeSet(path))
|
|
}
|
|
}
|
|
if u.carddavBackend != nil {
|
|
path, err := u.carddavBackend.AddressBookHomeSetPath(r.Context())
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
} else {
|
|
homeSets = append(homeSets, carddav.NewAddressBookHomeSet(path))
|
|
}
|
|
}
|
|
|
|
if r.URL.Path == userPrincipalPath {
|
|
opts := webdav.ServePrincipalOptions{
|
|
CurrentUserPrincipalPath: userPrincipalPath,
|
|
HomeSets: homeSets,
|
|
Capabilities: []webdav.Capability{
|
|
carddav.CapabilityAddressBook,
|
|
caldav.CapabilityCalendar,
|
|
},
|
|
}
|
|
|
|
webdav.ServePrincipal(w, r, &opts)
|
|
return
|
|
}
|
|
|
|
// TODO serve something on / that signals this being a DAV server?
|
|
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
}
|
|
|
|
func main() {
|
|
var (
|
|
addr string
|
|
authURL string
|
|
debug bool
|
|
jsonLog bool
|
|
storageURL string
|
|
cert string
|
|
key string
|
|
)
|
|
flag.StringVar(&addr, "addr", ":8080", "listening address")
|
|
flag.StringVar(&authURL, "auth.url", "", "auth backend URL (required)")
|
|
flag.StringVar(&storageURL, "storage.url", "", "storage backend URL (required)")
|
|
flag.StringVar(&cert, "cert", "", "certificate file for TLS")
|
|
flag.StringVar(&key, "key", "", "key file for TLS")
|
|
flag.BoolVar(&debug, "log.debug", false, "enable debug logs")
|
|
flag.BoolVar(&jsonLog, "log.json", false, "enable structured logs")
|
|
flag.Parse()
|
|
|
|
if len(flag.Args()) != 0 || authURL == "" || storageURL == "" {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
|
if debug {
|
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
|
}
|
|
if !jsonLog {
|
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
|
}
|
|
|
|
for _, method := range []string{
|
|
"PROPFIND",
|
|
"PROPPATCH",
|
|
"REPORT",
|
|
"MKCOL",
|
|
"COPY",
|
|
"MOVE",
|
|
} {
|
|
chi.RegisterMethod(method)
|
|
}
|
|
mux := chi.NewRouter()
|
|
mux.Use(middleware.Logger)
|
|
|
|
authProvider, err := auth.NewFromURL(authURL)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to load auth provider")
|
|
}
|
|
mux.Use(authProvider.Middleware())
|
|
|
|
upBackend := &userPrincipalBackend{}
|
|
|
|
caldavBackend, carddavBackend, err := storage.NewFromURL(storageURL, "/calendar/", "/contacts/", upBackend)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to load storage backend")
|
|
}
|
|
|
|
if (cert != "") != (key != "") {
|
|
log.Fatal().Msg("provide both cert and key for TLS")
|
|
}
|
|
|
|
carddavHandler := carddav.Handler{Backend: carddavBackend}
|
|
caldavHandler := caldav.Handler{Backend: caldavBackend}
|
|
handler := tokidokiHandler{
|
|
upBackend: upBackend,
|
|
authBackend: authProvider,
|
|
caldavBackend: caldavBackend,
|
|
carddavBackend: carddavBackend,
|
|
}
|
|
|
|
mux.Mount("/", &handler)
|
|
mux.Mount("/.well-known/caldav", &caldavHandler)
|
|
mux.Mount("/.well-known/carddav", &carddavHandler)
|
|
mux.Mount("/{user}/contacts", &carddavHandler)
|
|
mux.Mount("/{user}/calendar", &caldavHandler)
|
|
|
|
var ln net.Listener
|
|
if a := strings.TrimPrefix(addr, "unix://"); a != addr {
|
|
ln, err = net.Listen("unix", a)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to listen")
|
|
}
|
|
if err = os.Chmod(a, 0775); err != nil {
|
|
log.Warn().Err(err).Msg("failed to set socket mode")
|
|
}
|
|
} else {
|
|
ln, err = net.Listen("tcp", addr)
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("failed to listen")
|
|
}
|
|
}
|
|
|
|
server := http.Server{
|
|
Addr: addr,
|
|
Handler: mux,
|
|
}
|
|
|
|
log.Info().Str("address", addr).Msg("starting server")
|
|
log.Debug().Msg("debug output enabled")
|
|
|
|
go func() {
|
|
if (cert != "") && (key != "") {
|
|
err = server.ServeTLS(ln, cert, key)
|
|
} else {
|
|
err = server.Serve(ln)
|
|
}
|
|
if err != http.ErrServerClosed {
|
|
log.Fatal().Err(err).Msg("ListenAndServe() error")
|
|
}
|
|
}()
|
|
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
for sig := range sigCh {
|
|
switch sig {
|
|
case syscall.SIGINT, syscall.SIGTERM:
|
|
err := server.Shutdown(context.Background())
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("Shutdown() error")
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|