Update checker

This commit is contained in:
binwiederhier
2026-01-17 20:36:15 -05:00
parent 603273ab9d
commit cc9f9c0d24
8 changed files with 265 additions and 3 deletions

View File

@@ -1,6 +1,9 @@
package server
import (
"crypto/sha256"
"encoding/json"
"fmt"
"io/fs"
"net/netip"
"text/template"
@@ -275,3 +278,87 @@ func NewConfig() *Config {
WebPushExpiryWarningDuration: DefaultWebPushExpiryWarningDuration,
}
}
// configHashData is a subset of Config fields used for computing the config hash.
// It excludes sensitive fields (keys, passwords, tokens) and runtime-only fields.
type configHashData struct {
BaseURL string
ListenHTTP string
ListenHTTPS string
ListenUnix string
CacheDuration time.Duration
AttachmentTotalSizeLimit int64
AttachmentFileSizeLimit int64
AttachmentExpiryDuration time.Duration
KeepaliveInterval time.Duration
ManagerInterval time.Duration
DisallowedTopics []string
WebRoot string
MessageDelayMin time.Duration
MessageDelayMax time.Duration
MessageSizeLimit int
TotalTopicLimit int
VisitorSubscriptionLimit int
VisitorAttachmentTotalSizeLimit int64
VisitorAttachmentDailyBandwidthLimit int64
VisitorRequestLimitBurst int
VisitorRequestLimitReplenish time.Duration
VisitorMessageDailyLimit int
VisitorEmailLimitBurst int
VisitorEmailLimitReplenish time.Duration
EnableSignup bool
EnableLogin bool
RequireLogin bool
EnableReservations bool
EnableMetrics bool
EnablePayments bool
EnableCalls bool
EnableEmails bool
EnableWebPush bool
BillingContact string
Version string
}
// Hash computes a SHA-256 hash of the configuration. This is used to detect
// configuration changes for the web app version check feature.
func (c *Config) Hash() string {
data := configHashData{
BaseURL: c.BaseURL,
ListenHTTP: c.ListenHTTP,
ListenHTTPS: c.ListenHTTPS,
ListenUnix: c.ListenUnix,
CacheDuration: c.CacheDuration,
AttachmentTotalSizeLimit: c.AttachmentTotalSizeLimit,
AttachmentFileSizeLimit: c.AttachmentFileSizeLimit,
AttachmentExpiryDuration: c.AttachmentExpiryDuration,
KeepaliveInterval: c.KeepaliveInterval,
ManagerInterval: c.ManagerInterval,
DisallowedTopics: c.DisallowedTopics,
WebRoot: c.WebRoot,
MessageDelayMin: c.MessageDelayMin,
MessageDelayMax: c.MessageDelayMax,
MessageSizeLimit: c.MessageSizeLimit,
TotalTopicLimit: c.TotalTopicLimit,
VisitorSubscriptionLimit: c.VisitorSubscriptionLimit,
VisitorAttachmentTotalSizeLimit: c.VisitorAttachmentTotalSizeLimit,
VisitorAttachmentDailyBandwidthLimit: c.VisitorAttachmentDailyBandwidthLimit,
VisitorRequestLimitBurst: c.VisitorRequestLimitBurst,
VisitorRequestLimitReplenish: c.VisitorRequestLimitReplenish,
VisitorMessageDailyLimit: c.VisitorMessageDailyLimit,
VisitorEmailLimitBurst: c.VisitorEmailLimitBurst,
VisitorEmailLimitReplenish: c.VisitorEmailLimitReplenish,
EnableSignup: c.EnableSignup,
EnableLogin: c.EnableLogin,
RequireLogin: c.RequireLogin,
EnableReservations: c.EnableReservations,
EnableMetrics: c.EnableMetrics,
EnablePayments: c.StripeSecretKey != "",
EnableCalls: c.TwilioAccount != "",
EnableEmails: c.SMTPSenderFrom != "",
EnableWebPush: c.WebPushPublicKey != "",
BillingContact: c.BillingContact,
Version: c.Version,
}
b, _ := json.Marshal(data)
return fmt.Sprintf("%x", sha256.Sum256(b))
}

View File

@@ -90,6 +90,7 @@ var (
matrixPushPath = "/_matrix/push/v1/notify"
metricsPath = "/metrics"
apiHealthPath = "/v1/health"
apiVersionPath = "/v1/version"
apiStatsPath = "/v1/stats"
apiWebPushPath = "/v1/webpush"
apiTiersPath = "/v1/tiers"
@@ -460,6 +461,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == apiHealthPath {
return s.handleHealth(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == apiVersionPath {
return s.handleVersion(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == webManifestPath {
@@ -600,6 +603,14 @@ func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request, _ *visitor
return s.writeJSON(w, response)
}
func (s *Server) handleVersion(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
response := &apiVersionResponse{
Version: s.config.Version,
ConfigHash: s.config.Hash(),
}
return s.writeJSON(w, response)
}
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
response := &apiConfigResponse{
BaseURL: "", // Will translate to window.location.origin
@@ -615,6 +626,7 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
BillingContact: s.config.BillingContact,
WebPushPublicKey: s.config.WebPushPublicKey,
DisallowedTopics: s.config.DisallowedTopics,
ConfigHash: s.config.Hash(),
}
b, err := json.MarshalIndent(response, "", " ")
if err != nil {

View File

@@ -317,6 +317,11 @@ type apiHealthResponse struct {
Healthy bool `json:"healthy"`
}
type apiVersionResponse struct {
Version string `json:"version"`
ConfigHash string `json:"config_hash"`
}
type apiStatsResponse struct {
Messages int64 `json:"messages"`
MessagesRate float64 `json:"messages_rate"` // Average number of messages per second
@@ -482,6 +487,7 @@ type apiConfigResponse struct {
BillingContact string `json:"billing_contact"`
WebPushPublicKey string `json:"web_push_public_key"`
DisallowedTopics []string `json:"disallowed_topics"`
ConfigHash string `json:"config_hash"`
}
type apiAccountBillingPrices struct {