diff --git a/Makefile b/Makefile index 22c8924990..4d0aee176e 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,7 @@ OC_MODULES = \ services/gateway \ services/graph \ services/groups \ + services/groupware \ services/idm \ services/idp \ services/invitations \ diff --git a/opencloud/pkg/command/services.go b/opencloud/pkg/command/services.go index e87548979c..9c3602f186 100644 --- a/opencloud/pkg/command/services.go +++ b/opencloud/pkg/command/services.go @@ -25,6 +25,7 @@ import ( gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/command" graph "github.com/opencloud-eu/opencloud/services/graph/pkg/command" groups "github.com/opencloud-eu/opencloud/services/groups/pkg/command" + groupware "github.com/opencloud-eu/opencloud/services/groupware/pkg/command" idm "github.com/opencloud-eu/opencloud/services/idm/pkg/command" idp "github.com/opencloud-eu/opencloud/services/idp/pkg/command" invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/command" @@ -138,6 +139,11 @@ var svccmds = []register.Command{ cfg.Groups.Commons = cfg.Commons }) }, + func(cfg *config.Config) *cli.Command { + return ServiceCommand(cfg, cfg.Groupware.Service.Name, groupware.GetCommands(cfg.Groupware), func(c *config.Config) { + cfg.Groupware.Commons = cfg.Commons + }) + }, func(cfg *config.Config) *cli.Command { return ServiceCommand(cfg, cfg.IDM.Service.Name, idm.GetCommands(cfg.IDM), func(c *config.Config) { cfg.IDM.Commons = cfg.Commons diff --git a/opencloud/pkg/init/structs.go b/opencloud/pkg/init/structs.go index 89f836441a..1f2a554054 100644 --- a/opencloud/pkg/init/structs.go +++ b/opencloud/pkg/init/structs.go @@ -32,6 +32,7 @@ type OpenCloudConfig struct { AuthBearer AuthbearerService `yaml:"auth_bearer"` Users UsersAndGroupsService `yaml:"users"` Groups UsersAndGroupsService `yaml:"groups"` + Groupware GroupwareService `yaml:"groupware"` Ocdav InsecureService `yaml:"ocdav"` Ocm OcmService `yaml:"ocm"` Thumbnails ThumbnailService `yaml:"thumbnails"` @@ -126,6 +127,17 @@ type GraphService struct { ServiceAccount ServiceAccount `yaml:"service_account"` } +// GroupwareSettings is the configuration for the groupware settings +type GroupwareSettings struct { + WebdavAllowInsecure bool `yaml:"webdav_allow_insecure"` + Cs3AllowInsecure bool `yaml:"cs3_allow_insecure"` +} + +// GroupwareService is the configuration for the groupware service +type GroupwareService struct { + Groupware GroupwareSettings +} + // IdmService is the configuration for the IDM service type IdmService struct { ServiceUserPasswords ServiceUserPasswordsSettings `yaml:"service_user_passwords"` diff --git a/opencloud/pkg/runtime/service/service.go b/opencloud/pkg/runtime/service/service.go index a02bab58cf..86ad3843fd 100644 --- a/opencloud/pkg/runtime/service/service.go +++ b/opencloud/pkg/runtime/service/service.go @@ -35,6 +35,7 @@ import ( gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/command" graph "github.com/opencloud-eu/opencloud/services/graph/pkg/command" groups "github.com/opencloud-eu/opencloud/services/groups/pkg/command" + groupware "github.com/opencloud-eu/opencloud/services/groupware/pkg/command" idm "github.com/opencloud-eu/opencloud/services/idm/pkg/command" idp "github.com/opencloud-eu/opencloud/services/idp/pkg/command" invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/command" @@ -199,6 +200,11 @@ func NewService(ctx context.Context, options ...Option) (*Service, error) { cfg.Groups.Commons = cfg.Commons return groups.Execute(cfg.Groups) }) + reg(3, opts.Config.Groupware.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { + cfg.Groupware.Context = ctx + cfg.Groupware.Commons = cfg.Commons + return groupware.Execute(cfg.Groupware) + }) reg(3, opts.Config.IDM.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { cfg.IDM.Context = ctx cfg.IDM.Commons = cfg.Commons diff --git a/pkg/config/config.go b/pkg/config/config.go index 1e0c68a41a..dad2c2283a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -19,6 +19,7 @@ import ( gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/config" graph "github.com/opencloud-eu/opencloud/services/graph/pkg/config" groups "github.com/opencloud-eu/opencloud/services/groups/pkg/config" + groupware "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" idm "github.com/opencloud-eu/opencloud/services/idm/pkg/config" idp "github.com/opencloud-eu/opencloud/services/idp/pkg/config" invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/config" @@ -100,6 +101,7 @@ type Config struct { Gateway *gateway.Config `yaml:"gateway"` Graph *graph.Config `yaml:"graph"` Groups *groups.Config `yaml:"groups"` + Groupware *groupware.Config `yaml:"groupware"` IDM *idm.Config `yaml:"idm"` IDP *idp.Config `yaml:"idp"` Invitations *invitations.Config `yaml:"invitations"` diff --git a/pkg/config/defaultconfig.go b/pkg/config/defaultconfig.go index cc94e0bec2..fb2f7e5f66 100644 --- a/pkg/config/defaultconfig.go +++ b/pkg/config/defaultconfig.go @@ -19,6 +19,7 @@ import ( gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/config/defaults" graph "github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults" groups "github.com/opencloud-eu/opencloud/services/groups/pkg/config/defaults" + groupware "github.com/opencloud-eu/opencloud/services/groupware/pkg/config/defaults" idm "github.com/opencloud-eu/opencloud/services/idm/pkg/config/defaults" idp "github.com/opencloud-eu/opencloud/services/idp/pkg/config/defaults" invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/config/defaults" @@ -75,6 +76,7 @@ func DefaultConfig() *Config { Gateway: gateway.DefaultConfig(), Graph: graph.DefaultConfig(), Groups: groups.DefaultConfig(), + Groupware: groupware.DefaultConfig(), IDM: idm.DefaultConfig(), IDP: idp.DefaultConfig(), Invitations: invitations.DefaultConfig(), diff --git a/services/groupware/Makefile b/services/groupware/Makefile new file mode 100644 index 0000000000..b780ba9e92 --- /dev/null +++ b/services/groupware/Makefile @@ -0,0 +1,16 @@ +SHELL := bash +NAME := groupware + +ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI +include ../../.bingo/Variables.mk +endif + +include ../../.make/default.mk +include ../../.make/recursion.mk +include ../../.make/go.mk +include ../../.make/release.mk +include ../../.make/docs.mk + +.PHONY: go-generate +go-generate: $(MOCKERY) + $(MOCKERY) diff --git a/services/groupware/cmd/groupware/main.go b/services/groupware/cmd/groupware/main.go new file mode 100644 index 0000000000..ef1507eb49 --- /dev/null +++ b/services/groupware/cmd/groupware/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/opencloud-eu/opencloud/services/groupware/pkg/command" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config/defaults" +) + +func main() { + cfg := defaults.DefaultConfig() + cfg.Context, _ = signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP) + if err := command.Execute(cfg); err != nil { + os.Exit(1) + } +} diff --git a/services/groupware/pkg/command/root.go b/services/groupware/pkg/command/root.go new file mode 100644 index 0000000000..aa13cb6a5a --- /dev/null +++ b/services/groupware/pkg/command/root.go @@ -0,0 +1,27 @@ +package command + +import ( + "os" + + "github.com/opencloud-eu/opencloud/pkg/clihelper" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" + "github.com/urfave/cli/v2" +) + +// GetCommands provides all commands for this service +func GetCommands(cfg *config.Config) cli.Commands { + return []*cli.Command{ + Server(cfg), + Version(cfg), + } +} + +func Execute(cfg *config.Config) error { + app := clihelper.DefaultApp(&cli.App{ + Name: "groupware", + Usage: "Groupware service for OpenCloud", + Commands: GetCommands(cfg), + }) + + return app.RunContext(cfg.Context, os.Args) +} diff --git a/services/groupware/pkg/command/server.go b/services/groupware/pkg/command/server.go new file mode 100644 index 0000000000..613f0f3aa3 --- /dev/null +++ b/services/groupware/pkg/command/server.go @@ -0,0 +1,98 @@ +package command + +import ( + "context" + "fmt" + + "github.com/oklog/run" + "github.com/opencloud-eu/opencloud/pkg/config/configlog" + "github.com/opencloud-eu/opencloud/pkg/tracing" + "github.com/opencloud-eu/opencloud/pkg/version" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config/parser" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/logging" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/metrics" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/server/debug" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/server/http" + "github.com/urfave/cli/v2" +) + +// Server is the entrypoint for the server command. +func Server(cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "server", + Usage: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name), + Category: "server", + Before: func(_ *cli.Context) error { + return configlog.ReturnFatal(parser.ParseConfig(cfg)) + }, + Action: func(c *cli.Context) error { + logger := logging.Configure(cfg.Service.Name, cfg.Log) + + traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name) + if err != nil { + return err + } + + var ( + gr = run.Group{} + ctx, cancel = context.WithCancel(c.Context) + m = metrics.New() + ) + + defer cancel() + + m.BuildInfo.WithLabelValues(version.GetString()).Set(1) + + server, err := debug.Server( + debug.Logger(logger), + debug.Config(cfg), + debug.Context(ctx), + ) + if err != nil { + logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server") + return err + } + + gr.Add(server.ListenAndServe, func(_ error) { + _ = server.Shutdown(ctx) + cancel() + }) + + httpServer, err := http.Server( + http.Logger(logger), + http.Context(ctx), + http.Config(cfg), + http.Metrics(m), + http.Namespace(cfg.HTTP.Namespace), + http.TraceProvider(traceProvider), + ) + if err != nil { + logger.Info(). + Err(err). + Str("transport", "http"). + Msg("Failed to initialize server") + + return err + } + + gr.Add(httpServer.Run, func(_ error) { + if err == nil { + logger.Info(). + Str("transport", "http"). + Str("server", cfg.Service.Name). + Msg("Shutting down server") + } else { + logger.Error().Err(err). + Str("transport", "http"). + Str("server", cfg.Service.Name). + Msg("Shutting down server") + } + + cancel() + }) + + return gr.Run() + }, + } +} diff --git a/services/groupware/pkg/command/version.go b/services/groupware/pkg/command/version.go new file mode 100644 index 0000000000..b335cb2f12 --- /dev/null +++ b/services/groupware/pkg/command/version.go @@ -0,0 +1,26 @@ +package command + +import ( + "fmt" + + "github.com/opencloud-eu/opencloud/pkg/version" + + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" + "github.com/urfave/cli/v2" +) + +// Version prints the service versions of all running instances. +func Version(cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "version", + Usage: "print the version of this binary and the running service instances", + Category: "info", + Action: func(c *cli.Context) error { + fmt.Println("Version: " + version.GetString()) + fmt.Printf("Compiled: %s\n", version.Compiled()) + fmt.Println("") + + return nil + }, + } +} diff --git a/services/groupware/pkg/config/config.go b/services/groupware/pkg/config/config.go new file mode 100644 index 0000000000..d6ba3f50d4 --- /dev/null +++ b/services/groupware/pkg/config/config.go @@ -0,0 +1,36 @@ +package config + +import ( + "context" + + "github.com/opencloud-eu/opencloud/pkg/shared" +) + +// Config combines all available configuration parts. +type Config struct { + Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service + + Service Service `yaml:"-"` + + Tracing *Tracing `yaml:"tracing"` + Log *Log `yaml:"log"` + Debug Debug `yaml:"debug"` + + HTTP HTTP `yaml:"http"` + + Mail Mail `yaml:"mail"` + + Context context.Context `yaml:"-"` +} + +type MasterAuth struct { + Username string `yaml:"username" env:"OC_JMAP_MASTER_USERNAME;GROUPWARE_JMAP_MASTER_USERNAME"` + Password string `yaml:"password" env:"OC_JMAP_MASTER_PASSWORD;GROUPWARE_JMAP_MASTER_PASSWORD"` +} + +type Mail struct { + Master MasterAuth `yaml:"master"` + JmapUrl string `yaml:"jmap_url" env:"OC_JMAP_URL;GROUPWARE_JMAP_URL"` + CS3AllowInsecure bool `yaml:"cs3_allow_insecure" env:"OC_INSECURE;GROUPWARE_CS3SOURCE_INSECURE" desc:"Ignore untrusted SSL certificates when connecting to the CS3 source." introductionVersion:"1.0.0"` + RevaGateway string `yaml:"reva_gateway" env:"OC_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" introductionVersion:"1.0.0"` +} diff --git a/services/groupware/pkg/config/debug.go b/services/groupware/pkg/config/debug.go new file mode 100644 index 0000000000..9215b21837 --- /dev/null +++ b/services/groupware/pkg/config/debug.go @@ -0,0 +1,9 @@ +package config + +// Debug defines the available debug configuration. +type Debug struct { + Addr string `yaml:"addr" env:"GROUPWARE_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"1.0.0"` + Token string `yaml:"token" env:"GROUPWARE_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"` + Pprof bool `yaml:"pprof" env:"GROUPWARE_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"` + Zpages bool `yaml:"zpages" env:"GROUPWARE_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"1.0.0"` +} diff --git a/services/groupware/pkg/config/defaults/defaultconfig.go b/services/groupware/pkg/config/defaults/defaultconfig.go new file mode 100644 index 0000000000..69b7aa7d41 --- /dev/null +++ b/services/groupware/pkg/config/defaults/defaultconfig.go @@ -0,0 +1,89 @@ +package defaults + +import ( + "strings" + + "github.com/opencloud-eu/opencloud/pkg/shared" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" +) + +// FullDefaultConfig returns a fully initialized default configuration +func FullDefaultConfig() *config.Config { + cfg := DefaultConfig() + EnsureDefaults(cfg) + Sanitize(cfg) + return cfg +} + +// DefaultConfig returns a basic default configuration +func DefaultConfig() *config.Config { + return &config.Config{ + Debug: config.Debug{ + Addr: "127.0.0.1:9202", + Token: "", + Pprof: false, + Zpages: false, + }, + Mail: config.Mail{ + Master: config.MasterAuth{ + Username: "master", + Password: "secret", + }, + JmapUrl: "https://stalwart.opencloud.test/jmap", + RevaGateway: shared.DefaultRevaConfig().Address, + CS3AllowInsecure: false, + }, + HTTP: config.HTTP{ + Addr: "127.0.0.1:9276", + Root: "/groupware", + Namespace: "eu.opencloud.web", + CORS: config.CORS{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Cache-Control"}, + AllowCredentials: true, + }, + }, + Service: config.Service{ + Name: "groupware", + }, + } +} + +// EnsureDefaults adds default values to the configuration if they are not set yet +func EnsureDefaults(cfg *config.Config) { + // provide with defaults for shared logging, since we need a valid destination address for "envdecode". + if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil { + cfg.Log = &config.Log{ + Level: cfg.Commons.Log.Level, + Pretty: cfg.Commons.Log.Pretty, + Color: cfg.Commons.Log.Color, + File: cfg.Commons.Log.File, + } + } else if cfg.Log == nil { + cfg.Log = &config.Log{} + } + // provide with defaults for shared tracing, since we need a valid destination address for "envdecode". + if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil { + cfg.Tracing = &config.Tracing{ + Enabled: cfg.Commons.Tracing.Enabled, + Type: cfg.Commons.Tracing.Type, + Endpoint: cfg.Commons.Tracing.Endpoint, + Collector: cfg.Commons.Tracing.Collector, + } + } else if cfg.Tracing == nil { + cfg.Tracing = &config.Tracing{} + } + + if cfg.Commons != nil { + cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS + } + +} + +// Sanitize sanitized the configuration +func Sanitize(cfg *config.Config) { + if cfg.HTTP.Root != "/" { + cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/") + } +} diff --git a/services/groupware/pkg/config/http.go b/services/groupware/pkg/config/http.go new file mode 100644 index 0000000000..2907f26601 --- /dev/null +++ b/services/groupware/pkg/config/http.go @@ -0,0 +1,20 @@ +package config + +import "github.com/opencloud-eu/opencloud/pkg/shared" + +// CORS defines the available cors configuration. +type CORS struct { + AllowedOrigins []string `yaml:"allow_origins" env:"OC_CORS_ALLOW_ORIGINS;GROUPWARE_CORS_ALLOW_ORIGINS" desc:"A list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` + AllowedMethods []string `yaml:"allow_methods" env:"OC_CORS_ALLOW_METHODS;GROUPWARE_CORS_ALLOW_METHODS" desc:"A list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` + AllowedHeaders []string `yaml:"allow_headers" env:"OC_CORS_ALLOW_HEADERS;GROUPWARE_CORS_ALLOW_HEADERS" desc:"A list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` + AllowCredentials bool `yaml:"allow_credentials" env:"OC_CORS_ALLOW_CREDENTIALS;GROUPWARE_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials." introductionVersion:"1.0.0"` +} + +// HTTP defines the available http configuration. +type HTTP struct { + Addr string `yaml:"addr" env:"GROUPWARE_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"` + TLS shared.HTTPServiceTLS `yaml:"tls"` + Root string `yaml:"root" env:"GROUPWARE_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"1.0.0"` + Namespace string `yaml:"-"` + CORS CORS `yaml:"cors"` +} diff --git a/services/groupware/pkg/config/log.go b/services/groupware/pkg/config/log.go new file mode 100644 index 0000000000..4e3cea917f --- /dev/null +++ b/services/groupware/pkg/config/log.go @@ -0,0 +1,9 @@ +package config + +// Log defines the available log configuration. +type Log struct { + Level string `mapstructure:"level" env:"OC_LOG_LEVEL;THUMBNAILS_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"` + Pretty bool `mapstructure:"pretty" env:"OC_LOG_PRETTY;THUMBNAILS_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"1.0.0"` + Color bool `mapstructure:"color" env:"OC_LOG_COLOR;THUMBNAILS_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"1.0.0"` + File string `mapstructure:"file" env:"OC_LOG_FILE;THUMBNAILS_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"1.0.0"` +} diff --git a/services/groupware/pkg/config/parser/parse.go b/services/groupware/pkg/config/parser/parse.go new file mode 100644 index 0000000000..e02236f566 --- /dev/null +++ b/services/groupware/pkg/config/parser/parse.go @@ -0,0 +1,39 @@ +package parser + +import ( + "errors" + + occfg "github.com/opencloud-eu/opencloud/pkg/config" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config/defaults" + + "github.com/opencloud-eu/opencloud/pkg/config/envdecode" +) + +// ParseConfig loads configuration from known paths. +func ParseConfig(cfg *config.Config) error { + err := occfg.BindSourcesToStructs(cfg.Service.Name, cfg) + if err != nil { + return err + } + + defaults.EnsureDefaults(cfg) + + // load all env variables relevant to the config in the current context. + if err := envdecode.Decode(cfg); err != nil { + // no environment variable set for this config is an expected "error" + if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) { + return err + } + } + + // sanitize config + defaults.Sanitize(cfg) + + return Validate(cfg) +} + +// Validate can validate the configuration +func Validate(_ *config.Config) error { + return nil +} diff --git a/services/groupware/pkg/config/service.go b/services/groupware/pkg/config/service.go new file mode 100644 index 0000000000..d1eac383f0 --- /dev/null +++ b/services/groupware/pkg/config/service.go @@ -0,0 +1,6 @@ +package config + +// Service defines the available service configuration. +type Service struct { + Name string `yaml:"-"` +} diff --git a/services/groupware/pkg/config/tracing.go b/services/groupware/pkg/config/tracing.go new file mode 100644 index 0000000000..375b4024db --- /dev/null +++ b/services/groupware/pkg/config/tracing.go @@ -0,0 +1,21 @@ +package config + +import "github.com/opencloud-eu/opencloud/pkg/tracing" + +// Tracing defines the available tracing configuration. +type Tracing struct { + Enabled bool `yaml:"enabled" env:"OC_TRACING_ENABLED;ANTIVIRUS_TRACING_ENABLED" desc:"Activates tracing." introductionVersion:"1.0.0"` + Type string `yaml:"type" env:"OC_TRACING_TYPE;ANTIVIRUS_TRACING_TYPE" desc:"The type of tracing. Defaults to '', which is the same as 'jaeger'. Allowed tracing types are 'jaeger' and '' as of now." introductionVersion:"1.0.0"` + Endpoint string `yaml:"endpoint" env:"OC_TRACING_ENDPOINT;ANTIVIRUS_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent." introductionVersion:"1.0.0"` + Collector string `yaml:"collector" env:"OC_TRACING_COLLECTOR;ANTIVIRUS_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset." introductionVersion:"1.0.0"` +} + +// Convert Tracing to the tracing package's Config struct. +func (t Tracing) Convert() tracing.Config { + return tracing.Config{ + Enabled: t.Enabled, + Type: t.Type, + Endpoint: t.Endpoint, + Collector: t.Collector, + } +} diff --git a/services/groupware/pkg/jmap/email.go b/services/groupware/pkg/jmap/email.go new file mode 100644 index 0000000000..8bed85f418 --- /dev/null +++ b/services/groupware/pkg/jmap/email.go @@ -0,0 +1,41 @@ +package jmap + +import "time" + +type Email struct { + From string + Subject string + HasAttachments bool + Received time.Time +} + +func NewEmail(elem map[string]any) Email { + fromList := elem["from"].([]any) + from := fromList[0].(map[string]any) + var subject string + var value any = elem["subject"] + if value != nil { + subject = value.(string) + } else { + subject = "" + } + var hasAttachments bool + hasAttachmentsAny := elem["hasAttachments"] + if hasAttachmentsAny != nil { + hasAttachments = hasAttachmentsAny.(bool) + } else { + hasAttachments = false + } + + received, receivedErr := time.ParseInLocation(time.RFC3339, elem["receivedAt"].(string), time.UTC) + if receivedErr != nil { + panic(receivedErr) + } + + return Email{ + From: from["email"].(string), + Subject: subject, + HasAttachments: hasAttachments, + Received: received, + } +} diff --git a/services/groupware/pkg/jmap/jmap.go b/services/groupware/pkg/jmap/jmap.go new file mode 100644 index 0000000000..d44fc262ba --- /dev/null +++ b/services/groupware/pkg/jmap/jmap.go @@ -0,0 +1,360 @@ +package jmap + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "log/slog" + "net/http" +) + +type WellKnownJmap struct { + ApiUrl string `json:"apiUrl"` + PrimaryAccounts map[string]string `json:"primaryAccounts"` +} + +/* +func bearer(req *http.Request, token string) { + req.Header.Add("Authorization", "Bearer "+base64.StdEncoding.EncodeToString([]byte(token))) +} +*/ + +func fetch[T any](client *http.Client, url string, username string, password string, mapper func(body *[]byte) T) T { + req, reqErr := http.NewRequest(http.MethodGet, url, nil) + if reqErr != nil { + panic(reqErr) + } + req.SetBasicAuth(username, password) + + res, getErr := client.Do(req) + if getErr != nil { + panic(getErr) + } + if res.StatusCode != 200 { + panic(fmt.Sprintf("HTTP status code not 200: %d", res.StatusCode)) + } + if res.Body != nil { + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + log.Fatal(err) + } + }(res.Body) + } + + body, readErr := io.ReadAll(res.Body) + if readErr != nil { + log.Fatal(readErr) + } + + return mapper(&body) +} + +func simpleCommand(cmd string, params map[string]any) [][]any { + jmap := make([][]any, 1) + jmap[0] = make([]any, 3) + jmap[0][0] = cmd + jmap[0][1] = params + jmap[0][2] = "0" + return jmap +} + +const ( + JmapCore = "urn:ietf:params:jmap:core" + JmapMail = "urn:ietf:params:jmap:mail" +) + +func command[T any](client *http.Client, ctx context.Context, url string, username string, password string, methodCalls *[][]any, mapper func(body *[]byte) T) T { + jmapWrapper := map[string]any{ + "using": []string{JmapCore, JmapMail}, + "methodCalls": methodCalls, + } + + /* + { + "using":[ + "urn:ietf:params:jmap:core", + "urn:ietf:params:jmap:mail" + ], + "methodCalls":[ + [ + "Identity/get", { + "accountId": "cp" + }, "0" + ] + ] + } + */ + + bodyBytes, marshalErr := json.Marshal(jmapWrapper) + if marshalErr != nil { + panic(marshalErr) + } + + req, reqErr := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(bodyBytes)) + if reqErr != nil { + panic(reqErr) + } + req.SetBasicAuth(username, password) + req.Header.Add("Content-Type", "application/json") + + slog.Info("jmap", "url", url, "username", username) + res, postErr := client.Do(req) + if postErr != nil { + panic(postErr) + } + if res.StatusCode != 200 { + panic(fmt.Sprintf("HTTP status code not 200: %d", res.StatusCode)) + } + if res.Body != nil { + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + log.Fatal(err) + } + }(res.Body) + } + + body, readErr := io.ReadAll(res.Body) + if readErr != nil { + log.Fatal(readErr) + } + + if slog.Default().Enabled(ctx, slog.LevelDebug) { + slog.Debug(ctx.Value("operation").(string) + " response: " + string(body)) + } + + return mapper(&body) +} + +type JmapFolder struct { + Id string + Name string + Role string + TotalEmails int + UnreadEmails int + TotalThreads int + UnreadThreads int +} +type JmapFolders struct { + Folders []JmapFolder + state string +} + +type JmapCommandResponse struct { + MethodResponses [][]any `json:"methodResponses"` + SessionState string `json:"sessionState"` +} + +type JmapClient struct { + client *http.Client + username string + password string + url string + accountId string + ctx context.Context +} + +func New(client *http.Client, ctx context.Context, username string, password string, url string, accountId string) JmapClient { + return JmapClient{ + client: client, + ctx: ctx, + username: username, + password: password, + url: url, + accountId: accountId, + } +} + +func (jmap *JmapClient) FetchWellKnown() WellKnownJmap { + return fetch(jmap.client, jmap.url+"/.well-known/jmap", jmap.username, jmap.password, func(body *[]byte) WellKnownJmap { + var data WellKnownJmap + jsonErr := json.Unmarshal(*body, &data) + if jsonErr != nil { + panic(jsonErr) + } + + /* + u, urlErr := url.Parse(data.ApiUrl) + if urlErr != nil { + panic(urlErr) + } + jmap.url = jmap.url + u.Path + */ + jmap.accountId = data.PrimaryAccounts[JmapMail] + return data + }) +} + +func (jmap *JmapClient) GetMailboxes() JmapFolders { + /* + {"methodResponses": + [["Mailbox/get", + {"accountId":"cs","state":"n","list": + [{"id":"a","name":"Inbox","parentId":null,"role":"inbox","sortOrder":0,"isSubscribed":true,"totalEmails":0,"unreadEmails":0,"totalThreads":0,"unreadThreads":0,"myRights":{"mayReadItems":true,"mayAddItems":true,"mayRemoveItems":true,"maySetSeen":true,"maySetKeywords":true,"mayCreateChild":true,"mayRename":true,"mayDelete":true,"maySubmit":true}},{"id":"b","name":"Deleted Items","parentId":null,"role":"trash","sortOrder":0,"isSubscribed":true,"totalEmails":0,"unreadEmails":0,"totalThreads":0,"unreadThreads":0,"myRights":{"mayReadItems":true,"mayAddItems":true,"mayRemoveItems":true,"maySetSeen":true,"maySetKeywords":true,"mayCreateChild":true,"mayRename":true,"mayDelete":true,"maySubmit":true}},{"id":"c","name":"Junk Mail","parentId":null,"role":"junk","sortOrder":0,"isSubscribed":true,"totalEmails":0,"unreadEmails":0,"totalThreads":0,"unreadThreads":0,"myRights":{"mayReadItems":true,"mayAddItems":true,"mayRemoveItems":true,"maySetSeen":true,"maySetKeywords":true,"mayCreateChild":true,"mayRename":true,"mayDelete":true,"maySubmit":true}},{"id":"d","name":"Drafts","parentId":null,"role":"drafts","sortOrder":0,"isSubscribed":true,"totalEmails":0,"unreadEmails":0,"totalThreads":0,"unreadThreads":0,"myRights":{"mayReadItems":true,"mayAddItems":true,"mayRemoveItems":true,"maySetSeen":true,"maySetKeywords":true,"mayCreateChild":true,"mayRename":true,"mayDelete":true,"maySubmit":true}},{"id":"e","name":"Sent Items","parentId":null,"role":"sent","sortOrder":0,"isSubscribed":true,"totalEmails":0,"unreadEmails":0,"totalThreads":0,"unreadThreads":0,"myRights":{"mayReadItems":true,"mayAddItems":true,"mayRemoveItems":true,"maySetSeen":true,"maySetKeywords":true,"mayCreateChild":true,"mayRename":true,"mayDelete":true,"maySubmit":true}}],"notFound":[]},"0"]],"sessionState":"3e25b2a0"} + + */ + cmd := simpleCommand("Mailbox/get", map[string]any{"accountId": jmap.accountId}) + commandCtx := context.WithValue(jmap.ctx, "operation", "GetMailboxes") + return command(jmap.client, commandCtx, jmap.url, jmap.username, jmap.password, &cmd, func(body *[]byte) JmapFolders { + var data JmapCommandResponse + jsonErr := json.Unmarshal(*body, &data) + if jsonErr != nil { + panic(jsonErr) + } + first := data.MethodResponses[0] + params := first[1] + payload := params.(map[string]any) + state := payload["state"].(string) + list := payload["list"].([]any) + folders := make([]JmapFolder, len(list)) + for i, a := range list { + item := a.(map[string]any) + folders[i] = JmapFolder{ + Id: item["id"].(string), + Name: item["name"].(string), + Role: item["role"].(string), + TotalEmails: int(item["totalEmails"].(float64)), + UnreadEmails: int(item["unreadEmails"].(float64)), + TotalThreads: int(item["totalThreads"].(float64)), + UnreadThreads: int(item["unreadThreads"].(float64)), + } + } + return JmapFolders{Folders: folders, state: state} + }) +} + +type Emails struct { + Emails []Email + State string +} + +func (jmap *JmapClient) EmailQuery(mailboxId string) Emails { + cmd := make([][]any, 4) + cmd[0] = []any{ + "Email/query", + map[string]any{ + "accountId": jmap.accountId, + "filter": map[string]any{ + "inMailbox": mailboxId, + }, + "sort": []map[string]any{ + { + "isAscending": false, + "property": "receivedAt", + }, + }, + "collapseThreads": true, + "position": 0, + "limit": 30, + "calculateTotal": true, + }, + "0", + } + cmd[1] = []any{ + "Email/get", + map[string]any{ + "accountId": jmap.accountId, + "#ids": map[string]any{ + "resultOf": "0", + "name": "Email/query", + "path": "/ids", + }, + "properties": []string{"threadId"}, + }, + "1", + } + cmd[2] = []any{ + "Thread/get", + map[string]any{ + "accountId": jmap.accountId, + "#ids": map[string]any{ + "resultOf": "1", + "name": "Email/get", + "path": "/list/*/threadId", + }, + }, + "2", + } + cmd[3] = []any{ + "Email/get", + map[string]any{ + "accountId": jmap.accountId, + "#ids": map[string]any{ + "resultOf": "2", + "name": "Thread/get", + "path": "/list/*/emailIds", + }, + "properties": []string{ + "threadId", + "mailboxIds", + "keywords", + "hasAttachment", + "from", + "subject", + "receivedAt", + "size", + "preview", + }, + }, + "3", + } + + commandCtx := context.WithValue(jmap.ctx, "operation", "GetMailboxes") + return command(jmap.client, commandCtx, jmap.url, jmap.username, jmap.password, &cmd, func(body *[]byte) Emails { + var data JmapCommandResponse + jsonErr := json.Unmarshal(*body, &data) + if jsonErr != nil { + panic(jsonErr) + } + matches := make([][]any, 1) + for _, elem := range data.MethodResponses { + if elem[0] == "Email/get" && elem[2] == "3" { + matches = append(matches, elem) + } + } + /* + matches := lo.Filter(data.MethodResponses, func(elem []any, index int) bool { + return elem[0] == "Email/get" && elem[2] == "3" + }) + */ + payload := matches[0][1].(map[string]any) + list := payload["list"].([]any) + + /* + { + "threadId": "cc", + "mailboxIds": { + "a": true + }, + "keywords": {}, + "hasAttachment": false, + "from": [ + { + "name": null, + "email": "root@nsa.gov" + } + ], + "subject": "Hello 5", + "receivedAt": "2025-04-10T13:07:27Z", + "size": 47, + "preview": "Hi <3", + "id": "iiaaaaaa" + }, + */ + + emails := make([]Email, len(list)) + for i, elem := range list { + emails[i] = NewEmail(elem.(map[string]any)) + } + /* + emails := lo.Map(list, func(elem any, _ int) Email { + return NewEmail(elem.(map[string]any)) + }) + */ + return Emails{Emails: emails, State: data.SessionState} + }) +} diff --git a/services/groupware/pkg/logging/logging.go b/services/groupware/pkg/logging/logging.go new file mode 100644 index 0000000000..c8756ebcc3 --- /dev/null +++ b/services/groupware/pkg/logging/logging.go @@ -0,0 +1,17 @@ +package logging + +import ( + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" +) + +// Configure initializes a service-specific logger instance. +func Configure(name string, cfg *config.Log) log.Logger { + return log.NewLogger( + log.Name(name), + log.Level(cfg.Level), + log.Pretty(cfg.Pretty), + log.Color(cfg.Color), + log.File(cfg.File), + ) +} diff --git a/services/groupware/pkg/metrics/metrics.go b/services/groupware/pkg/metrics/metrics.go new file mode 100644 index 0000000000..7d9fa58ec9 --- /dev/null +++ b/services/groupware/pkg/metrics/metrics.go @@ -0,0 +1,34 @@ +package metrics + +import "github.com/prometheus/client_golang/prometheus" + +var ( + // Namespace defines the namespace for the defines metrics. + Namespace = "opencloud" + + // Subsystem defines the subsystem for the defines metrics. + Subsystem = "groupware" +) + +// Metrics defines the available metrics of this service. +type Metrics struct { + BuildInfo *prometheus.GaugeVec +} + +// New initializes the available metrics. +func New() *Metrics { + m := &Metrics{ + BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "build_info", + Help: "Build information", + }, []string{"version"}), + } + + _ = prometheus.Register( + m.BuildInfo, + ) + + return m +} diff --git a/services/groupware/pkg/server/debug/option.go b/services/groupware/pkg/server/debug/option.go new file mode 100644 index 0000000000..64ff971d09 --- /dev/null +++ b/services/groupware/pkg/server/debug/option.go @@ -0,0 +1,50 @@ +package debug + +import ( + "context" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" +) + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + Logger log.Logger + Context context.Context + Config *config.Config +} + +// newOptions initializes the available default options. +func newOptions(opts ...Option) Options { + opt := Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// Logger provides a function to set the logger option. +func Logger(val log.Logger) Option { + return func(o *Options) { + o.Logger = val + } +} + +// Context provides a function to set the context option. +func Context(val context.Context) Option { + return func(o *Options) { + o.Context = val + } +} + +// Config provides a function to set the config option. +func Config(val *config.Config) Option { + return func(o *Options) { + o.Config = val + } +} diff --git a/services/groupware/pkg/server/debug/server.go b/services/groupware/pkg/server/debug/server.go new file mode 100644 index 0000000000..3f54c66012 --- /dev/null +++ b/services/groupware/pkg/server/debug/server.go @@ -0,0 +1,24 @@ +package debug + +import ( + "net/http" + + "github.com/opencloud-eu/opencloud/pkg/handlers" + "github.com/opencloud-eu/opencloud/pkg/service/debug" + "github.com/opencloud-eu/opencloud/pkg/version" +) + +// Server initializes the debug service and server. +func Server(opts ...Option) (*http.Server, error) { + options := newOptions(opts...) + + readyHandlerConfiguration := handlers.NewCheckHandlerConfiguration(). + WithLogger(options.Logger) + + return debug.NewService( + debug.Logger(options.Logger), + debug.Name(options.Config.Service.Name), + debug.Version(version.GetString()), + debug.Ready(handlers.NewCheckHandler(readyHandlerConfiguration)), + ), nil +} diff --git a/services/groupware/pkg/server/http/option.go b/services/groupware/pkg/server/http/option.go new file mode 100644 index 0000000000..eb54b7288c --- /dev/null +++ b/services/groupware/pkg/server/http/option.go @@ -0,0 +1,83 @@ +package http + +import ( + "context" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/metrics" + "github.com/urfave/cli/v2" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" +) + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + Namespace string + Logger log.Logger + Context context.Context + Config *config.Config + Metrics *metrics.Metrics + Flags []cli.Flag + TraceProvider trace.TracerProvider +} + +// newOptions initializes the available default options. +func newOptions(opts ...Option) Options { + opt := Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// Logger provides a function to set the logger option. +func Logger(val log.Logger) Option { + return func(o *Options) { + o.Logger = val + } +} + +// Context provides a function to set the context option. +func Context(val context.Context) Option { + return func(o *Options) { + o.Context = val + } +} + +// Config provides a function to set the config option. +func Config(val *config.Config) Option { + return func(o *Options) { + o.Config = val + } +} + +// Metrics provides a function to set the metrics option. +func Metrics(val *metrics.Metrics) Option { + return func(o *Options) { + o.Metrics = val + } +} + +// Namespace provides a function to set the Namespace option. +func Namespace(val string) Option { + return func(o *Options) { + o.Namespace = val + } +} + +// TraceProvider provides a function to configure the trace provider +func TraceProvider(traceProvider trace.TracerProvider) Option { + return func(o *Options) { + if traceProvider != nil { + o.TraceProvider = traceProvider + } else { + o.TraceProvider = noop.NewTracerProvider() + } + } +} diff --git a/services/groupware/pkg/server/http/server.go b/services/groupware/pkg/server/http/server.go new file mode 100644 index 0000000000..67c9073917 --- /dev/null +++ b/services/groupware/pkg/server/http/server.go @@ -0,0 +1,67 @@ +package http + +import ( + "fmt" + + "github.com/go-chi/chi/v5/middleware" + "github.com/opencloud-eu/opencloud/pkg/cors" + opencloudmiddleware "github.com/opencloud-eu/opencloud/pkg/middleware" + "github.com/opencloud-eu/opencloud/pkg/service/http" + "github.com/opencloud-eu/opencloud/pkg/version" + svc "github.com/opencloud-eu/opencloud/services/groupware/pkg/service/http/v0" + "go-micro.dev/v4" +) + +// Server initializes the http service and server. +func Server(opts ...Option) (http.Service, error) { + options := newOptions(opts...) + + service, err := http.NewService( + http.TLSConfig(options.Config.HTTP.TLS), + http.Logger(options.Logger), + http.Name(options.Config.Service.Name), + http.Version(version.GetString()), + http.Namespace(options.Config.HTTP.Namespace), + http.Address(options.Config.HTTP.Addr), + http.Context(options.Context), + http.TraceProvider(options.TraceProvider), + ) + if err != nil { + options.Logger.Error(). + Err(err). + Msg("Error initializing http service") + return http.Service{}, fmt.Errorf("could not initialize http service: %w", err) + } + + handle := svc.NewService( + svc.Logger(options.Logger), + svc.Config(options.Config), + svc.Middleware( + middleware.RealIP, + middleware.RequestID, + opencloudmiddleware.Cors( + cors.Logger(options.Logger), + cors.AllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins), + cors.AllowedMethods(options.Config.HTTP.CORS.AllowedMethods), + cors.AllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders), + cors.AllowCredentials(options.Config.HTTP.CORS.AllowCredentials), + ), + opencloudmiddleware.Version( + options.Config.Service.Name, + version.GetString(), + ), + opencloudmiddleware.Logger(options.Logger), + ), + ) + + { + handle = svc.NewInstrument(handle, options.Metrics) + handle = svc.NewLogging(handle, options.Logger) + } + + if err := micro.RegisterHandler(service.Server(), handle); err != nil { + return http.Service{}, err + } + + return service, nil +} diff --git a/services/groupware/pkg/service/http/v0/instrument.go b/services/groupware/pkg/service/http/v0/instrument.go new file mode 100644 index 0000000000..d3b17817fe --- /dev/null +++ b/services/groupware/pkg/service/http/v0/instrument.go @@ -0,0 +1,25 @@ +package svc + +import ( + "net/http" + + "github.com/opencloud-eu/opencloud/services/groupware/pkg/metrics" +) + +// NewInstrument returns a service that instruments metrics. +func NewInstrument(next Service, metrics *metrics.Metrics) Service { + return instrument{ + next: next, + metrics: metrics, + } +} + +type instrument struct { + next Service + metrics *metrics.Metrics +} + +// ServeHTTP implements the Service interface. +func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) { + i.next.ServeHTTP(w, r) +} diff --git a/services/groupware/pkg/service/http/v0/logging.go b/services/groupware/pkg/service/http/v0/logging.go new file mode 100644 index 0000000000..c21734ce11 --- /dev/null +++ b/services/groupware/pkg/service/http/v0/logging.go @@ -0,0 +1,25 @@ +package svc + +import ( + "net/http" + + "github.com/opencloud-eu/opencloud/pkg/log" +) + +// NewLogging returns a service that logs messages. +func NewLogging(next Service, logger log.Logger) Service { + return logging{ + next: next, + logger: logger, + } +} + +type logging struct { + next Service + logger log.Logger +} + +// ServeHTTP implements the Service interface. +func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) { + l.next.ServeHTTP(w, r) +} diff --git a/services/groupware/pkg/service/http/v0/option.go b/services/groupware/pkg/service/http/v0/option.go new file mode 100644 index 0000000000..d39a415d14 --- /dev/null +++ b/services/groupware/pkg/service/http/v0/option.go @@ -0,0 +1,52 @@ +package svc + +import ( + "net/http" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" + "go.opentelemetry.io/otel/trace" +) + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + Logger log.Logger + Config *config.Config + Middleware []func(http.Handler) http.Handler + TraceProvider trace.TracerProvider +} + +// newOptions initializes the available default options. +func newOptions(opts ...Option) Options { + opt := Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// Logger provides a function to set the logger option. +func Logger(val log.Logger) Option { + return func(o *Options) { + o.Logger = val + } +} + +// Config provides a function to set the config option. +func Config(val *config.Config) Option { + return func(o *Options) { + o.Config = val + } +} + +// Middleware provides a function to set the middleware option. +func Middleware(val ...func(http.Handler) http.Handler) Option { + return func(o *Options) { + o.Middleware = val + } +} diff --git a/services/groupware/pkg/service/http/v0/service.go b/services/groupware/pkg/service/http/v0/service.go new file mode 100644 index 0000000000..fcdbc26975 --- /dev/null +++ b/services/groupware/pkg/service/http/v0/service.go @@ -0,0 +1,95 @@ +package svc + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/riandyrn/otelchi" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/pkg/tracing" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/config" + "github.com/opencloud-eu/opencloud/services/groupware/pkg/jmap" +) + +/* +type contextKey string + +const ( + keyContextKey contextKey = "key" +) +*/ + +// Service defines the service handlers. +type Service interface { + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +// NewService returns a service implementation for Service. +func NewService(opts ...Option) Service { + options := newOptions(opts...) + + m := chi.NewMux() + m.Use(options.Middleware...) + + m.Use( + otelchi.Middleware( + "groupware", + otelchi.WithChiRoutes(m), + otelchi.WithTracerProvider(options.TraceProvider), + otelchi.WithPropagators(tracing.GetPropagator()), + ), + ) + + svc := Groupware{ + config: options.Config, + mux: m, + logger: options.Logger, + } + + m.Route(options.Config.HTTP.Root, func(r chi.Router) { + r.Get("/", svc.WellDefined) + r.Get("/ping", svc.Ping) + }) + + _ = chi.Walk(m, func(method string, route string, _ http.Handler, middlewares ...func(http.Handler) http.Handler) error { + options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint") + return nil + }) + + return svc +} + +// Thumbnails implements the business logic for Service. +type Groupware struct { + config *config.Config + logger log.Logger + mux *chi.Mux + httpClient *http.Client +} + +// ServeHTTP implements the Service interface. +func (s Groupware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.mux.ServeHTTP(w, r) +} + +type IndexResponse struct { + AccountId string +} + +func (IndexResponse) Render(w http.ResponseWriter, r *http.Request) error { + return nil +} + +func (g Groupware) Ping(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) +} + +func (g Groupware) WellDefined(w http.ResponseWriter, r *http.Request) { + //logger := g.logger.SubloggerWithRequestID(r.Context()) + + client := jmap.New(g.httpClient, r.Context(), "alan", "demo", "https://stalwart.opencloud.test/jmap", "cs") + wellKnown := client.FetchWellKnown() + _ = render.Render(w, r, IndexResponse{AccountId: wellKnown.PrimaryAccounts[jmap.JmapMail]}) +}