tokidoki/cmd/tokidoki/main.go
2024-11-07 17:55:56 +01:00

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
}
}
}