groupware: fix typos and minor issues

- fix a bunch of minor issues and typos that were found using GoLand
   and gosec

 - add a gosec Makefile target for Groupware related files, in
   services/groupware/Makefile

 - enable checking JMAP session capabilities for events and contacts,
   and only enable skipping that check for tasks until those are
   implemented in Stalwart as well

 - fix a CWE-190 (integer overflow or wraparound) found by gosec

 - consistently use struct references for methods of Groupware and
   Request, instead of mixing up references and copies

 - always log errors when unable to register a Prometheus metric
This commit is contained in:
Pascal Bleser
2026-03-04 17:05:38 +01:00
parent e141a7c8e0
commit cf824b5447
15 changed files with 184 additions and 143 deletions

View File

@@ -555,15 +555,16 @@ type HttpWsClient struct {
}
func (w *HttpWsClient) readPump() {
logger := log.From(w.logger.With().Str("username", w.username))
defer func() {
w.c.Close()
if err := w.c.Close(); err != nil {
logger.Warn().Err(err).Msg("failed to close websocket connection")
}
}()
//w.c.SetReadLimit(maxMessageSize)
//c.conn.SetReadDeadline(time.Now().Add(pongWait))
//c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
logger := log.From(w.logger.With().Str("username", w.username))
for {
if _, message, err := w.c.ReadMessage(); err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {

View File

@@ -100,7 +100,7 @@ const (
JmapSieve = "urn:ietf:params:jmap:sieve"
JmapBlob = "urn:ietf:params:jmap:blob"
JmapQuota = "urn:ietf:params:jmap:quota"
JmapWebsocket = "urn:ietf:params:jmap:websocket"
JmapWebsocket = "urn:ietf:params:jmap:websocket" // #nosec G101 false positive: these are not credentials
JmapPrincipals = "urn:ietf:params:jmap:principals"
JmapPrincipalsOwner = "urn:ietf:params:jmap:principals:owner"
JmapTasks = "urn:ietf:params:jmap:tasks"

View File

@@ -44,3 +44,7 @@ examples:
cd ../../pkg/jscontact/ && go test -tags=groupware_examples . -v -count=1 -run '^.*Example$''
cd ../../pkg/jscalendar/ && go test -tags=groupware_examples . -v -count=1 -run '^.*Example$''
cd ./pkg/groupware/ && go test -tags=groupware_examples . -v -count=1 -run '^.*Example$''
.PHONY: gosec
gosec:
cd ../../ && gosec ./pkg/jmap/... ./pkg/jscalendar/... ./pkg/jscontact/... ./services/groupware/pkg/...

View File

@@ -16,10 +16,9 @@ process.stdin.on('end', () => {
process.stdout.write("\n")
} catch (error) {
if (error instanceof Error) {
console.error(`Error occured while post-processing HTML: ${error.message}`)
console.error(`Error occurred while post-processing HTML: ${error.message}`)
} else {
console.error("Unknown error occurred")
}
}
});

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io"
"math"
"mime"
"net/http"
"slices"
@@ -1293,9 +1294,17 @@ func relatedEmailsFilter(email jmap.Email, beacon time.Time, days uint) jmap.Ema
}
}
timeFilter := jmap.EmailFilterCondition{
Before: beacon.Add(time.Duration(days) * time.Hour * 24),
After: beacon.Add(time.Duration(-days) * time.Hour * 24),
var timeFilter jmap.EmailFilterCondition
{
if days > math.MaxInt64 {
days = math.MaxInt64 // avoid gosec G115 (CWE-190)
}
hours := int64(days) * 24
delta := time.Duration(hours) * time.Hour
timeFilter = jmap.EmailFilterCondition{
Before: beacon.Add(delta),
After: beacon.Add(-delta),
}
}
var filter jmap.EmailFilterElement
@@ -1382,7 +1391,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
if results, ok := resultsByAccountId[accountId]; ok {
duration := time.Since(before)
if jerr != nil {
req.observeJmapError(jerr)
_ = req.observeJmapError(jerr)
l.Error().Err(jerr).Msgf("failed to query %v emails", RelationTypeSameSender)
} else {
req.observe(g.metrics.EmailSameSenderDuration.WithLabelValues(req.session.JmapEndpoint), duration.Seconds())
@@ -1402,7 +1411,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
emails, _, _, _, jerr := g.jmap.EmailsInThread(accountId, email.ThreadId, req.session, bgctx, l, req.language(), false, g.config.maxBodyValueBytes)
duration := time.Since(before)
if jerr != nil {
req.observeJmapError(jerr)
_ = req.observeJmapError(jerr)
l.Error().Err(jerr).Msgf("failed to list %v emails", RelationTypeSameThread)
} else {
req.observe(g.metrics.EmailSameThreadDuration.WithLabelValues(req.session.JmapEndpoint), duration.Seconds())
@@ -1735,8 +1744,8 @@ var sanitizableMediaTypes = []string{
"text/xhtml",
}
func (req *Request) sanitizeEmail(source jmap.Email) (jmap.Email, *Error) {
if !req.g.config.sanitize {
func (r *Request) sanitizeEmail(source jmap.Email) (jmap.Email, *Error) {
if !r.g.config.sanitize {
return source, nil
}
memory := map[string]int{}
@@ -1746,8 +1755,8 @@ func (req *Request) sanitizeEmail(source jmap.Email) (jmap.Email, *Error) {
t, _, err := mime.ParseMediaType(p.Type)
if err != nil {
msg := fmt.Sprintf("failed to parse the mime type '%s'", p.Type)
req.logger.Error().Str("type", log.SafeString(p.Type)).Msg(msg)
return source, req.apiError(&ErrorFailedToSanitizeEmail, withDetail(msg))
r.logger.Error().Str("type", log.SafeString(p.Type)).Msg(msg)
return source, r.apiError(&ErrorFailedToSanitizeEmail, withDetail(msg))
}
if slices.Contains(sanitizableMediaTypes, t) {
if already, done := memory[p.PartId]; !done {
@@ -1783,13 +1792,13 @@ func (req *Request) sanitizeEmail(source jmap.Email) (jmap.Email, *Error) {
return source, nil
}
func (req *Request) sanitizeEmails(source []jmap.Email) ([]jmap.Email, *Error) {
if !req.g.config.sanitize {
func (r *Request) sanitizeEmails(source []jmap.Email) ([]jmap.Email, *Error) {
if !r.g.config.sanitize {
return source, nil
}
result := make([]jmap.Email, len(source))
for i, email := range source {
sanitized, gwerr := req.sanitizeEmail(email)
sanitized, gwerr := r.sanitizeEmail(email)
if gwerr != nil {
return nil, gwerr
}

View File

@@ -35,7 +35,7 @@ func NewDnsSessionUrlResolver(
dialTimeout time.Duration,
readTimeout time.Duration,
) (DnsSessionUrlResolver, error) {
// TODO the whole udp or tcp dialier configuration, see https://github.com/miekg/exdns/blob/master/q/q.go
// TODO the whole udp or tcp dialer configuration, see https://github.com/miekg/exdns/blob/master/q/q.go
c := &dns.Client{
DialTimeout: dialTimeout,
@@ -45,6 +45,8 @@ func NewDnsSessionUrlResolver(
return DnsSessionUrlResolver{
defaultSessionUrlSupplier: defaultSessionUrlSupplier,
defaultDomain: defaultDomain,
domainGreenList: domainGreenList,
domainRedList: domainRedList,
config: config,
client: c,
}, nil

View File

@@ -587,7 +587,7 @@ func errorId(r *http.Request, ctx context.Context) string {
}
}
func (r Request) errorId() string {
func (r *Request) errorId() string {
return errorId(r.r, r.ctx)
}
@@ -608,11 +608,11 @@ func apiError(id string, gwerr GroupwareError, options ...ErrorOpt) *Error {
return err
}
func (r Request) observedParameterError(gwerr GroupwareError, options ...ErrorOpt) *Error {
func (r *Request) observedParameterError(gwerr GroupwareError, options ...ErrorOpt) *Error {
return r.observeParameterError(apiError(r.errorId(), gwerr, options...))
}
func (r Request) apiError(err *GroupwareError, options ...ErrorOpt) *Error {
func (r *Request) apiError(err *GroupwareError, options ...ErrorOpt) *Error {
if err == nil {
return nil
}
@@ -620,7 +620,7 @@ func (r Request) apiError(err *GroupwareError, options ...ErrorOpt) *Error {
return apiError(errorId, *err, options...)
}
func (r Request) apiErrorFromJmap(err jmap.Error) *Error {
func (r *Request) apiErrorFromJmap(err jmap.Error) *Error {
if err == nil {
return nil
}
@@ -637,6 +637,6 @@ func errorResponses(errors ...Error) ErrorResponse {
return ErrorResponse{Errors: errors}
}
func (r Request) errorResponseFromJmap(accountIds []string, err jmap.Error) Response {
func (r *Request) errorResponseFromJmap(accountIds []string, err jmap.Error) Response {
return errorResponse(accountIds, r.apiErrorFromJmap(r.observeJmapError(err)))
}

View File

@@ -230,7 +230,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
httpTransport.ResponseHeaderTimeout = responseHeaderTimeout
if insecureTls {
tlsConfig := &tls.Config{InsecureSkipVerify: true}
tlsConfig := &tls.Config{InsecureSkipVerify: true} // #nosec G402 insecure TLS is a configuration option for development
httpTransport.TLSClientConfig = tlsConfig
}
httpClient = *http.DefaultClient
@@ -242,6 +242,11 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
auth,
jmapMetricsAdapter,
)
defer func() {
if err := api.Close(); err != nil {
logger.Error().Err(err).Msgf("failed to close HTTP JMAP API client")
}
}()
}
var wsf *jmap.HttpWsClientFactory
@@ -250,7 +255,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
HandshakeTimeout: wsHandshakeTimeout,
}
if insecureTls {
wsDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
wsDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // #nosec G402 insecure TLS is a configuration option for development
}
wsf, err = jmap.NewHttpWsClientFactory(wsDialer, auth, logger, jmapMetricsAdapter)
@@ -262,6 +267,11 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
// api implements all three interfaces:
jmapClient = jmap.NewClient(api, api, api, wsf)
defer func() {
if err := jmapClient.Close(); err != nil {
logger.Error().Err(err).Msgf("failed to close JMAP client")
}
}()
}
sessionCacheBuilder := newSessionCacheBuilder(
@@ -297,7 +307,6 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
}
sessionCache, err := sessionCacheBuilder.build()
if err != nil {
// assuming that the error was logged in great detail upstream
return nil, GroupwareInitializationError{Message: "failed to initialize the session cache", Err: err}
@@ -311,11 +320,15 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
if err != nil {
logger.Warn().Err(err).Msgf("failed to create metric %v", m.EventBufferSizeDesc.String())
} else {
prometheusRegistry.Register(metrics.ConstMetricCollector{Metric: eventBufferSizeMetric})
if err := prometheusRegistry.Register(metrics.ConstMetricCollector{Metric: eventBufferSizeMetric}); err != nil {
logger.Error().Err(err).Msg("failed to register event buffer size metric collector")
}
}
prometheusRegistry.Register(prometheus.NewGaugeFunc(m.EventBufferQueuedOpts, func() float64 {
if err := prometheusRegistry.Register(prometheus.NewGaugeFunc(m.EventBufferQueuedOpts, func() float64 {
return float64(len(eventChannel))
}))
})); err != nil {
logger.Error().Err(err).Msg("failed to reigster event buffer queue metric")
}
}
sseServer := sse.New()
@@ -328,9 +341,11 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
sseServer.OnUnsubscribe = func(streamID string, sub *sse.Subscriber) {
sseSubscribers.Add(-1)
}
prometheusRegistry.Register(prometheus.NewGaugeFunc(m.SSESubscribersOpts, func() float64 {
if err := prometheusRegistry.Register(prometheus.NewGaugeFunc(m.SSESubscribersOpts, func() float64 {
return float64(sseSubscribers.Load())
}))
})); err != nil {
logger.Error().Err(err).Msg("failed to register SSE subscribers metric")
}
}
jobsChannel := make(chan Job, workerQueueSize)
@@ -339,12 +354,16 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
if err != nil {
logger.Warn().Err(err).Msgf("failed to create metric %v", m.WorkersBufferSizeDesc.String())
} else {
prometheusRegistry.Register(metrics.ConstMetricCollector{Metric: totalWorkerBufferMetric})
if err := prometheusRegistry.Register(metrics.ConstMetricCollector{Metric: totalWorkerBufferMetric}); err != nil {
logger.Error().Err(err).Msg("failed to register total worker buffer metric")
}
}
prometheusRegistry.Register(prometheus.NewGaugeFunc(m.WorkersBufferQueuedOpts, func() float64 {
if err := prometheusRegistry.Register(prometheus.NewGaugeFunc(m.WorkersBufferQueuedOpts, func() float64 {
return float64(len(jobsChannel))
}))
})); err != nil {
logger.Error().Err(err).Msg("failed to register jobs channel size metric")
}
}
var busyWorkers atomic.Int32
@@ -353,12 +372,16 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
if err != nil {
logger.Warn().Err(err).Msgf("failed to create metric %v", m.TotalWorkersDesc.String())
} else {
prometheusRegistry.Register(metrics.ConstMetricCollector{Metric: totalWorkersMetric})
if err := prometheusRegistry.Register(metrics.ConstMetricCollector{Metric: totalWorkersMetric}); err != nil {
logger.Error().Err(err).Msg("failed to register worker pool size metric")
}
}
prometheusRegistry.Register(prometheus.NewGaugeFunc(m.BusyWorkersOpts, func() float64 {
if err := prometheusRegistry.Register(prometheus.NewGaugeFunc(m.BusyWorkersOpts, func() float64 {
return float64(busyWorkers.Load())
}))
})); err != nil {
logger.Error().Err(err).Msg("failed to register busy workers metric")
}
}
g := &Groupware{
@@ -501,7 +524,7 @@ func (g *Groupware) session(ctx context.Context, user user, logger *log.Logger)
return jmap.Session{}, false, s.Error(), s.Until()
}
}
// not sure this should/could happen:
// not sure whether this should/could happen:
logger.Warn().Msg("session cache returned nil")
return jmap.Session{}, false, nil, time.Time{}
}
@@ -554,7 +577,9 @@ func (g *Groupware) serveError(w http.ResponseWriter, r *http.Request, error *Er
}
render.Status(r, error.NumStatus)
w.WriteHeader(error.NumStatus)
render.Render(w, r, errorResponses(*error))
if err := render.Render(w, r, errorResponses(*error)); err != nil {
g.logger.Error().Err(err).Msgf("failed to render error response")
}
}
// Execute a closure with a JMAP Session.
@@ -644,7 +669,9 @@ func (g *Groupware) sendResponse(w http.ResponseWriter, r *http.Request, respons
g.log(response.err)
w.Header().Add("Content-Type", ContentTypeJsonApi)
render.Status(r, response.err.NumStatus)
render.Render(w, r, errorResponses(*response.err))
if err := render.Render(w, r, errorResponses(*response.err)); err != nil {
g.logger.Error().Err(err).Msgf("failed to render error response")
}
return
}
@@ -757,7 +784,9 @@ func (g *Groupware) stream(w http.ResponseWriter, r *http.Request, handler func(
w.Header().Add("Content-Type", ContentTypeJsonApi)
render.Status(r, apierr.NumStatus)
w.WriteHeader(apierr.NumStatus)
render.Render(w, r, errorResponses(*apierr))
if err := render.Render(w, r, errorResponses(*apierr)); err != nil {
logger.Error().Err(err).Msgf("failed to render error response")
}
}
}

View File

@@ -26,8 +26,8 @@ import (
)
const (
// TODO remove this once Stalwart has actual support for Tasks, Calendars, Contacts and we don't need to mock it any more
IgnoreSessionCapabilityChecks = true
// TODO remove this once Stalwart has actual support for Tasks and we don't need to mock it any more
IgnoreSessionCapabilityChecksForTasks = true
)
// using a wrapper class for requests, to group multiple parameters, really to avoid crowding the
@@ -42,23 +42,23 @@ type Request struct {
session *jmap.Session
}
func isDefaultAccountid(accountId string) bool {
func isDefaultAccountId(accountId string) bool {
return slices.Contains(defaultAccountIds, accountId)
}
func (r Request) push(typ string, event any) {
func (r *Request) push(typ string, event any) {
r.g.push(r.user, typ, event)
}
func (r Request) GetUser() user {
func (r *Request) GetUser() user {
return r.user
}
func (r Request) GetRequestId() string {
func (r *Request) GetRequestId() string {
return chimiddleware.GetReqID(r.ctx)
}
func (r Request) GetTraceId() string {
func (r *Request) GetTraceId() string {
return groupwaremiddleware.GetTraceID(r.ctx)
}
@@ -76,7 +76,7 @@ var (
// errNoPrimaryAccountForWebsocket = errors.New("no primary account for websocket")
)
func (r Request) HeaderParam(name string) (string, *Error) {
func (r *Request) HeaderParam(name string) (string, *Error) {
value := r.r.Header.Get(name)
if value == "" {
msg := fmt.Sprintf("Missing mandatory request header '%s'", name)
@@ -89,19 +89,19 @@ func (r Request) HeaderParam(name string) (string, *Error) {
}
}
func (r Request) HeaderParamDoc(name string, _ string) (string, *Error) {
func (r *Request) HeaderParamDoc(name string, _ string) (string, *Error) {
return r.HeaderParam(name)
}
func (r Request) OptHeaderParam(name string) string {
func (r *Request) OptHeaderParam(name string) string {
return r.r.Header.Get(name)
}
func (r Request) OptHeaderParamDoc(name string, _ string) string {
func (r *Request) OptHeaderParamDoc(name string, _ string) string {
return r.OptHeaderParam(name)
}
func (r Request) PathParam(name string) (string, *Error) {
func (r *Request) PathParam(name string) (string, *Error) {
value := chi.URLParam(r.r, name)
if value == "" {
msg := fmt.Sprintf("Missing mandatory path parameter '%s'", name)
@@ -114,11 +114,11 @@ func (r Request) PathParam(name string) (string, *Error) {
}
}
func (r Request) PathParamDoc(name string, _ string) (string, *Error) {
func (r *Request) PathParamDoc(name string, _ string) (string, *Error) {
return r.PathParam(name)
}
func (r Request) PathListParamDoc(name string, _ string) ([]string, *Error) {
func (r *Request) PathListParamDoc(name string, _ string) ([]string, *Error) {
value, err := r.PathParam(name)
if err != nil {
return nil, err
@@ -126,14 +126,14 @@ func (r Request) PathListParamDoc(name string, _ string) ([]string, *Error) {
return strings.Split(value, ","), nil
}
func (r Request) AllAccountIds() []string {
func (r *Request) AllAccountIds() []string {
// TODO potentially filter on "subscribed" accounts?
return structs.Uniq(structs.Keys(r.session.Accounts))
}
func (r Request) GetAccountIdWithoutFallback() (string, *Error) {
func (r *Request) GetAccountIdWithoutFallback() (string, *Error) {
accountId := chi.URLParam(r.r, UriParamAccountId)
if accountId == "" || isDefaultAccountid(accountId) {
if accountId == "" || isDefaultAccountId(accountId) {
r.logger.Error().Err(errNoPrimaryAccountFallback).Msg("failed to determine the accountId")
return "", apiError(r.errorId(), ErrorNonExistingAccount,
withDetail("Failed to determine the account to use"),
@@ -143,9 +143,9 @@ func (r Request) GetAccountIdWithoutFallback() (string, *Error) {
return accountId, nil
}
func (r Request) getAccountId(fallback string, err error) (string, *Error) {
func (r *Request) getAccountId(fallback string, err error) (string, *Error) {
accountId := chi.URLParam(r.r, UriParamAccountId)
if accountId == "" || isDefaultAccountid(accountId) {
if accountId == "" || isDefaultAccountId(accountId) {
accountId = fallback
}
if accountId == "" {
@@ -158,45 +158,45 @@ func (r Request) getAccountId(fallback string, err error) (string, *Error) {
return accountId, nil
}
func (r Request) GetAccountIdForMail() (string, *Error) {
func (r *Request) GetAccountIdForMail() (string, *Error) {
return r.getAccountId(r.session.PrimaryAccounts.Mail, errNoPrimaryAccountForMail)
}
func (r Request) GetAccountIdForBlob() (string, *Error) {
func (r *Request) GetAccountIdForBlob() (string, *Error) {
return r.getAccountId(r.session.PrimaryAccounts.Blob, errNoPrimaryAccountForBlob)
}
func (r Request) GetAccountIdForVacationResponse() (string, *Error) {
func (r *Request) GetAccountIdForVacationResponse() (string, *Error) {
return r.getAccountId(r.session.PrimaryAccounts.VacationResponse, errNoPrimaryAccountForVacationResponse)
}
func (r Request) GetAccountIdForQuota() (string, *Error) {
func (r *Request) GetAccountIdForQuota() (string, *Error) {
return r.getAccountId(r.session.PrimaryAccounts.Quota, errNoPrimaryAccountForQuota)
}
func (r Request) GetAccountIdForSubmission() (string, *Error) {
func (r *Request) GetAccountIdForSubmission() (string, *Error) {
return r.getAccountId(r.session.PrimaryAccounts.Blob, errNoPrimaryAccountForSubmission)
}
func (r Request) GetAccountIdForTask() (string, *Error) {
func (r *Request) GetAccountIdForTask() (string, *Error) {
// TODO we don't have these yet, not implemented in Stalwart
// return r.getAccountId(r.session.PrimaryAccounts.Task, errNoPrimaryAccountForTask)
return r.GetAccountIdForMail()
}
func (r Request) GetAccountIdForCalendar() (string, *Error) {
func (r *Request) GetAccountIdForCalendar() (string, *Error) {
// TODO we don't have these yet, not implemented in Stalwart
// return r.getAccountId(r.session.PrimaryAccounts.Calendar, errNoPrimaryAccountForCalendar)
return r.GetAccountIdForMail()
}
func (r Request) GetAccountIdForContact() (string, *Error) {
func (r *Request) GetAccountIdForContact() (string, *Error) {
// TODO we don't have these yet, not implemented in Stalwart
// return r.getAccountId(r.session.PrimaryAccounts.Contact, errNoPrimaryAccountForContact)
return r.GetAccountIdForMail()
}
func (r Request) GetAccountForMail() (string, jmap.Account, *Error) {
func (r *Request) GetAccountForMail() (string, jmap.Account, *Error) {
accountId, err := r.GetAccountIdForMail()
if err != nil {
return "", jmap.Account{}, err
@@ -214,17 +214,17 @@ func (r Request) GetAccountForMail() (string, jmap.Account, *Error) {
return accountId, account, nil
}
func (r Request) parameterError(param string, detail string) *Error {
func (r *Request) parameterError(param string, detail string) *Error {
return r.observedParameterError(ErrorInvalidRequestParameter,
withDetail(detail),
withSource(&ErrorSource{Parameter: param}))
}
func (r Request) parameterErrorResponse(accountIds []string, param string, detail string) Response {
func (r *Request) parameterErrorResponse(accountIds []string, param string, detail string) Response {
return errorResponse(accountIds, r.parameterError(param, detail))
}
func (r Request) getStringParam(param string, defaultValue string) (string, bool) {
func (r *Request) getStringParam(param string, defaultValue string) (string, bool) {
q := r.r.URL.Query()
if !q.Has(param) {
return defaultValue, false
@@ -236,7 +236,7 @@ func (r Request) getStringParam(param string, defaultValue string) (string, bool
return str, true
}
func (r Request) getMandatoryStringParam(param string) (string, *Error) {
func (r *Request) getMandatoryStringParam(param string) (string, *Error) {
str := ""
q := r.r.URL.Query()
if q.Has(param) {
@@ -252,7 +252,7 @@ func (r Request) getMandatoryStringParam(param string) (string, *Error) {
return str, nil
}
func (r Request) parseIntParam(param string, defaultValue int) (int, bool, *Error) {
func (r *Request) parseIntParam(param string, defaultValue int) (int, bool, *Error) {
q := r.r.URL.Query()
if !q.Has(param) {
return defaultValue, false, nil
@@ -276,7 +276,7 @@ func (r Request) parseIntParam(param string, defaultValue int) (int, bool, *Erro
return int(value), true, nil
}
func (r Request) parseUIntParam(param string, defaultValue uint) (uint, bool, *Error) {
func (r *Request) parseUIntParam(param string, defaultValue uint) (uint, bool, *Error) {
q := r.r.URL.Query()
if !q.Has(param) {
return defaultValue, false, nil
@@ -300,7 +300,7 @@ func (r Request) parseUIntParam(param string, defaultValue uint) (uint, bool, *E
return uint(value), true, nil
}
func (r Request) parseDateParam(param string) (time.Time, bool, *Error) {
func (r *Request) parseDateParam(param string) (time.Time, bool, *Error) {
q := r.r.URL.Query()
if !q.Has(param) {
return time.Time{}, false, nil
@@ -322,7 +322,7 @@ func (r Request) parseDateParam(param string) (time.Time, bool, *Error) {
return t, true, nil
}
func (r Request) parseBoolParam(param string, defaultValue bool) (bool, bool, *Error) {
func (r *Request) parseBoolParam(param string, defaultValue bool) (bool, bool, *Error) {
q := r.r.URL.Query()
if !q.Has(param) {
return defaultValue, false, nil
@@ -344,7 +344,7 @@ func (r Request) parseBoolParam(param string, defaultValue bool) (bool, bool, *E
return b, true, nil
}
func (r Request) parseMapParam(param string) (map[string]string, bool, *Error) {
func (r *Request) parseMapParam(param string) (map[string]string, bool, *Error) {
q := r.r.URL.Query()
if !q.Has(param) {
return map[string]string{}, false, nil
@@ -363,7 +363,7 @@ func (r Request) parseMapParam(param string) (map[string]string, bool, *Error) {
return result, true, nil
}
func (r Request) parseOptStringListParam(param string) ([]string, bool, *Error) {
func (r *Request) parseOptStringListParam(param string) ([]string, bool, *Error) {
result := []string{}
q := r.r.URL.Query()
if !q.Has(param) {
@@ -379,11 +379,11 @@ func (r Request) parseOptStringListParam(param string) ([]string, bool, *Error)
return result, true, nil
}
func (r Request) bodydoc(target any, _ string) *Error {
func (r *Request) bodydoc(target any, _ string) *Error {
return r.body(target)
}
func (r Request) body(target any) *Error {
func (r *Request) body(target any) *Error {
body := r.r.Body
defer func(b io.ReadCloser) {
err := b.Close()
@@ -400,30 +400,30 @@ func (r Request) body(target any) *Error {
return nil
}
func (r Request) language() string {
func (r *Request) language() string {
return r.r.Header.Get("Accept-Language")
}
func (r Request) observe(obs prometheus.Observer, value float64) {
func (r *Request) observe(obs prometheus.Observer, value float64) {
metrics.WithExemplar(obs, value, r.GetRequestId(), r.GetTraceId())
}
func (r Request) observeParameterError(err *Error) *Error {
func (r *Request) observeParameterError(err *Error) *Error {
if err != nil {
r.g.metrics.ParameterErrorCounter.WithLabelValues(err.Code).Inc()
}
return err
}
func (r Request) observeJmapError(jerr jmap.Error) jmap.Error {
func (r *Request) observeJmapError(jerr jmap.Error) jmap.Error {
if jerr != nil {
r.g.metrics.JmapErrorCounter.WithLabelValues(r.session.JmapEndpoint, strconv.Itoa(jerr.Code())).Inc()
}
return jerr
}
func (r Request) needTask(accountId string) (bool, Response) {
if !IgnoreSessionCapabilityChecks {
func (r *Request) needTask(accountId string) (bool, Response) {
if !IgnoreSessionCapabilityChecksForTasks {
if r.session.Capabilities.Tasks == nil {
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingTasksSessionCapability), r.session.State)
}
@@ -431,7 +431,7 @@ func (r Request) needTask(accountId string) (bool, Response) {
return true, Response{}
}
func (r Request) needTaskForAccount(accountId string) (bool, Response) {
func (r *Request) needTaskForAccount(accountId string) (bool, Response) {
if ok, resp := r.needTask(accountId); !ok {
return ok, resp
}
@@ -439,37 +439,31 @@ func (r Request) needTaskForAccount(accountId string) (bool, Response) {
if !ok {
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State)
}
if !IgnoreSessionCapabilityChecks {
if account.AccountCapabilities.Tasks == nil {
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingTasksAccountCapability), r.session.State)
}
if account.AccountCapabilities.Tasks == nil {
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingTasksAccountCapability), r.session.State)
}
return true, Response{}
}
func (r Request) needTaskWithAccount() (bool, string, Response) {
func (r *Request) needTaskWithAccount() (bool, string, Response) {
accountId, err := r.GetAccountIdForTask()
if err != nil {
return false, "", errorResponse(single(accountId), err)
}
if !IgnoreSessionCapabilityChecks {
if ok, resp := r.needTaskForAccount(accountId); !ok {
return false, accountId, resp
}
if ok, resp := r.needTaskForAccount(accountId); !ok {
return false, accountId, resp
}
return true, accountId, Response{}
}
func (r Request) needCalendar(accountId string) (bool, Response) {
if !IgnoreSessionCapabilityChecks {
if r.session.Capabilities.Calendars == nil {
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingCalendarsSessionCapability), r.session.State)
}
func (r *Request) needCalendar(accountId string) (bool, Response) {
if r.session.Capabilities.Calendars == nil {
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingCalendarsSessionCapability), r.session.State)
}
return true, Response{}
}
func (r Request) needCalendarForAccount(accountId string) (bool, Response) {
func (r *Request) needCalendarForAccount(accountId string) (bool, Response) {
if ok, resp := r.needCalendar(accountId); !ok {
return ok, resp
}
@@ -477,35 +471,31 @@ func (r Request) needCalendarForAccount(accountId string) (bool, Response) {
if !ok {
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State)
}
if !IgnoreSessionCapabilityChecks {
if account.AccountCapabilities.Calendars == nil {
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingCalendarsAccountCapability), r.session.State)
}
if account.AccountCapabilities.Calendars == nil {
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingCalendarsAccountCapability), r.session.State)
}
return true, Response{}
}
func (r Request) needCalendarWithAccount() (bool, string, Response) {
func (r *Request) needCalendarWithAccount() (bool, string, Response) {
accountId, err := r.GetAccountIdForCalendar()
if err != nil {
return false, "", errorResponse(single(accountId), err)
}
if !IgnoreSessionCapabilityChecks {
if ok, resp := r.needCalendarForAccount(accountId); !ok {
return false, accountId, resp
}
if ok, resp := r.needCalendarForAccount(accountId); !ok {
return false, accountId, resp
}
return true, accountId, Response{}
}
func (r Request) needContact(accountId string) (bool, Response) {
func (r *Request) needContact(accountId string) (bool, Response) {
if r.session.Capabilities.Contacts == nil {
return false, errorResponseWithSessionState(single(accountId), r.apiError(&ErrorMissingContactsSessionCapability), r.session.State)
}
return true, Response{}
}
func (r Request) needContactForAccount(accountId string) (bool, Response) {
func (r *Request) needContactForAccount(accountId string) (bool, Response) {
if ok, resp := r.needContact(accountId); !ok {
return ok, resp
}
@@ -519,7 +509,7 @@ func (r Request) needContactForAccount(accountId string) (bool, Response) {
return true, Response{}
}
func (r Request) needContactWithAccount() (bool, string, Response) {
func (r *Request) needContactWithAccount() (bool, string, Response) {
accountId, err := r.GetAccountIdForContact()
if err != nil {
return false, "", errorResponse(single(accountId), err)

View File

@@ -23,14 +23,14 @@ func newRevaContextUsernameProvider() userProvider {
}
var (
errUserNotInRevaContext = errors.New("failed to find user in reva context")
errUserNotInRevaContext = errors.New("failed to find user in Reva context")
)
func (r revaContextUsernameProvider) GetUser(req *http.Request, ctx context.Context, logger *log.Logger) (user, error) {
func (r revaContextUsernameProvider) GetUser(_ *http.Request, ctx context.Context, logger *log.Logger) (user, error) {
u, ok := revactx.ContextGetUser(ctx)
if !ok {
err := errUserNotInRevaContext
logger.Error().Err(err).Ctx(ctx).Msgf("could not get user: user not in reva context: %v", ctx)
logger.Error().Err(err).Ctx(ctx).Msgf("could not get user: user not in Reva context: %v", ctx)
return nil, err
}
return revaUser{user: u}, nil
@@ -86,14 +86,14 @@ const (
var tokenMissingInRevaContext = RevaError{
code: revaErrorTokenMissingInRevaContext,
err: errors.New("Token is missing from Reva context"),
err: errors.New("token is missing from Reva context"),
}
func (h *RevaBearerHttpJmapClientAuthenticator) Authenticate(ctx context.Context, username string, logger *log.Logger, req *http.Request) jmap.Error {
func (h *RevaBearerHttpJmapClientAuthenticator) Authenticate(ctx context.Context, _ string, logger *log.Logger, req *http.Request) jmap.Error {
token, ok := revactx.ContextGetToken(ctx)
if !ok {
err := tokenMissingInRevaContext
logger.Error().Err(err).Ctx(ctx).Msgf("could not get token: token not in reva context: %v", ctx)
logger.Error().Err(err).Ctx(ctx).Msgf("could not get token: token not in Reva context: %v", ctx)
return err
} else {
req.Header.Add("Authorization", "Bearer "+token)
@@ -101,11 +101,11 @@ func (h *RevaBearerHttpJmapClientAuthenticator) Authenticate(ctx context.Context
}
}
func (h *RevaBearerHttpJmapClientAuthenticator) AuthenticateWS(ctx context.Context, username string, logger *log.Logger, headers http.Header) jmap.Error {
func (h *RevaBearerHttpJmapClientAuthenticator) AuthenticateWS(ctx context.Context, _ string, logger *log.Logger, headers http.Header) jmap.Error {
token, ok := revactx.ContextGetToken(ctx)
if !ok {
err := tokenMissingInRevaContext
logger.Error().Err(err).Ctx(ctx).Msgf("could not get token: token not in reva context: %v", ctx)
logger.Error().Err(err).Ctx(ctx).Msgf("could not get token: token not in Reva context: %v", ctx)
return err
} else {
headers.Add("Authorization", "Bearer "+token)

View File

@@ -117,7 +117,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Post("/", g.SendEmail)
r.Patch("/", g.UpdateEmail)
r.Delete("/", g.DeleteEmail)
Report(r, "/", g.RelatedToEmail)
report(r, "/", g.RelatedToEmail)
r.Route("/related", func(r chi.Router) {
r.Get("/", g.RelatedToEmail)
})
@@ -178,6 +178,6 @@ func (g *Groupware) Route(r chi.Router) {
r.MethodNotAllowed(g.MethodNotAllowed)
}
func Report(r chi.Router, pattern string, h http.HandlerFunc) {
func report(r chi.Router, pattern string, h http.HandlerFunc) {
r.MethodFunc("REPORT", pattern, h)
}

View File

@@ -116,25 +116,25 @@ type ttlcacheSessionCache struct {
var _ sessionCache = &ttlcacheSessionCache{}
var _ jmap.SessionEventListener = &ttlcacheSessionCache{}
func (l *ttlcacheSessionCache) load(key sessionCacheKey, ctx context.Context) cachedSession {
func (c *ttlcacheSessionCache) load(key sessionCacheKey, ctx context.Context) cachedSession {
username := key.username()
sessionUrl, gwerr := l.sessionUrlProvider(ctx, username)
sessionUrl, gwerr := c.sessionUrlProvider(ctx, username)
if gwerr != nil {
l.logger.Warn().Str(logUsername, username).Str(logErrorCode, gwerr.Code).Msgf("failed to determine session URL for '%v'", key)
c.logger.Warn().Str(logUsername, username).Str(logErrorCode, gwerr.Code).Msgf("failed to determine session URL for '%v'", key)
now := time.Now()
until := now.Add(l.errorTtl)
until := now.Add(c.errorTtl)
return failedSession{since: now, until: until, err: gwerr}
}
session, jerr := l.sessionSupplier(ctx, sessionUrl, username, l.logger)
session, jerr := c.sessionSupplier(ctx, sessionUrl, username, c.logger)
if jerr != nil {
l.logger.Warn().Str(logUsername, username).Err(jerr).Msgf("failed to create session for '%v'", key)
c.logger.Warn().Str(logUsername, username).Err(jerr).Msgf("failed to create session for '%v'", key)
now := time.Now()
until := now.Add(l.errorTtl)
until := now.Add(c.errorTtl)
return failedSession{since: now, until: until, err: groupwareErrorFromJmap(jerr)}
} else {
l.logger.Debug().Str(logUsername, username).Msgf("successfully created session for '%v'", key)
c.logger.Debug().Str(logUsername, username).Msgf("successfully created session for '%v'", key)
now := time.Now()
until := now.Add(l.successTtl)
until := now.Add(c.successTtl)
return succeededSession{since: now, until: until, session: session}
}
}
@@ -229,7 +229,7 @@ func (b *sessionCacheBuilder) withDnsAutoDiscovery(
return b
}
func (b sessionCacheBuilder) build() (sessionCache, error) {
func (b *sessionCacheBuilder) build() (sessionCache, error) {
var cache *ttlcache.Cache[sessionCacheKey, cachedSession]
sessionUrlResolver, err := b.sessionUrlResolverFactory()
@@ -243,7 +243,9 @@ func (b sessionCacheBuilder) build() (sessionCache, error) {
ttlcache.WithDisableTouchOnHit[sessionCacheKey, cachedSession](),
)
b.prometheusRegistry.Register(sessionCacheMetricsCollector{desc: b.m.SessionCacheDesc, supply: cache.Metrics})
if err := b.prometheusRegistry.Register(sessionCacheMetricsCollector{desc: b.m.SessionCacheDesc, supply: cache.Metrics}); err != nil {
b.logger.Error().Err(err).Msg("failed to register session cache metrics")
}
cache.OnEviction(func(c context.Context, r ttlcache.EvictionReason, item *ttlcache.Item[sessionCacheKey, cachedSession]) {
if b.logger.Trace().Enabled() {
@@ -291,7 +293,7 @@ func (b sessionCacheBuilder) build() (sessionCache, error) {
return s, nil
}
func (c ttlcacheSessionCache) OnSessionOutdated(session *jmap.Session, newSessionState jmap.SessionState) {
func (c *ttlcacheSessionCache) OnSessionOutdated(session *jmap.Session, newSessionState jmap.SessionState) {
// it's enough to remove the session from the cache, as it will be fetched on-demand
// the next time an operation is performed on behalf of the user
c.sessionCache.Delete(toSessionCacheKey(session.Username))

View File

@@ -324,7 +324,9 @@ func (r *LoggingPrometheusRegisterer) Register(c prometheus.Collector) error {
func (r *LoggingPrometheusRegisterer) MustRegister(collectors ...prometheus.Collector) {
for _, c := range collectors {
r.Register(c)
if err := r.Register(c); err != nil {
r.logger.Error().Err(err).Msg("failed to register metrics collector")
}
}
}

View File

@@ -3,19 +3,20 @@ package metrics
import (
"sync/atomic"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/version"
"github.com/prometheus/client_golang/prometheus"
)
var registered atomic.Bool
func StartupMetrics(registerer prometheus.Registerer) {
func StartupMetrics(registerer prometheus.Registerer, logger *log.Logger) {
// use an atomic boolean to make the operation idempotent,
// instead of causing a panic in case this function is
// called twice
if registered.CompareAndSwap(false, true) {
// https://github.com/prometheus/common/blob/8558a5b7db3c84fa38b4766966059a7bd5bfa2ee/version/info.go#L36-L56
registerer.Register(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
if err := registerer.Register(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "build_info",
@@ -23,6 +24,8 @@ func StartupMetrics(registerer prometheus.Registerer) {
ConstLabels: prometheus.Labels{
"version": version.GetString(),
},
}, func() float64 { return 1 }))
}, func() float64 { return 1 })); err != nil {
logger.Error().Err(err).Msg("failed to register startup metrics")
}
}
}

View File

@@ -58,7 +58,7 @@ func NewService(opts ...Option) (Service, error) {
}
}
metrics.StartupMetrics(registerer)
metrics.StartupMetrics(registerer, logger)
return gw, nil
}