/****************************************************************************** * * Description : * * Setup & initialization. * *****************************************************************************/ package main //go:generate protoc --go_out=../pbx --go_opt=paths=source_relative --go-grpc_out=../pbx --go-grpc_opt=paths=source_relative ../pbx/model.proto import ( "context" "encoding/json" "errors" "flag" "net/http" "os" "runtime" "runtime/pprof" "strings" "time" gh "github.com/gorilla/handlers" // For stripping comments from JSON config jcr "github.com/tinode/jsonco" // Authenticators "github.com/tinode/chat/server/auth" _ "github.com/tinode/chat/server/auth/anon" _ "github.com/tinode/chat/server/auth/basic" _ "github.com/tinode/chat/server/auth/code" _ "github.com/tinode/chat/server/auth/rest" _ "github.com/tinode/chat/server/auth/token" adapter "github.com/tinode/chat/server/db" "github.com/tinode/chat/server/store/types" // Database backends _ "github.com/tinode/chat/server/db/mongodb" _ "github.com/tinode/chat/server/db/mysql" _ "github.com/tinode/chat/server/db/postgres" _ "github.com/tinode/chat/server/db/rethinkdb" "github.com/tinode/chat/server/logs" // Push notifications "github.com/tinode/chat/server/push" _ "github.com/tinode/chat/server/push/fcm" _ "github.com/tinode/chat/server/push/jpush" _ "github.com/tinode/chat/server/push/stdout" _ "github.com/tinode/chat/server/push/tnpg" "github.com/tinode/chat/server/store" // Credential validators _ "github.com/tinode/chat/server/validate/email" _ "github.com/tinode/chat/server/validate/tel" "google.golang.org/grpc" // File upload handlers _ "github.com/tinode/chat/server/media/fs" _ "github.com/tinode/chat/server/media/oss" _ "github.com/tinode/chat/server/media/s3" ) const ( // currentVersion is the current API/protocol version currentVersion = "0.25" // minSupportedVersion is the minimum supported API version minSupportedVersion = "0.20" // idleSessionTimeout defines duration of being idle before terminating a session. idleSessionTimeout = time.Second * 45 // idleMasterTopicTimeout defines now long to keep master topic alive after the last session detached. idleMasterTopicTimeout = time.Second * 4 // Same as above but shut down the proxy topic sooner. Otherwise master topic would be kept alive for too long. idleProxyTopicTimeout = time.Second * 2 // defaultMaxMessageSize is the default maximum message size defaultMaxMessageSize = 1 << 19 // 512K // defaultMaxSubscriberCount is the default maximum number of group topic subscribers. // Also set in adapter. defaultMaxSubscriberCount = 256 // defaultMaxTagCount is the default maximum number of indexable tags defaultMaxTagCount = 16 // minTagLength is the shortest acceptable length of a tag in runes. Shorter tags are discarded. minTagLength = 2 // maxTagLength is the maximum length of a tag in runes. Longer tags are trimmed. maxTagLength = 96 // Delay before updating a User Agent uaTimerDelay = time.Second * 5 // maxDeleteCount is the maximum allowed number of messages to delete in one call. defaultMaxDeleteCount = 1024 // Base URL path for serving the streaming API. defaultApiPath = "/" // Mount point where static content is served, http://host-name defaultStaticMount = "/" // Local path to static content defaultStaticPath = "static" // Default country code to fall back to if the "default_country_code" field // isn't specified in the config. defaultCountryCode = "US" // Default timeout to drop an unanswered call, seconds. defaultCallEstablishmentTimeout = 30 ) // Build version number defined by the compiler: // // -ldflags "-X main.buildstamp=value_to_assign_to_buildstamp" // // Reported to clients in response to {hi} message. // For instance, to define the buildstamp as a timestamp of when the server was built add a // flag to compiler command line: // // -ldflags "-X main.buildstamp=`date -u '+%Y%m%dT%H:%M:%SZ'`" // // or to set it to git tag: // // -ldflags "-X main.buildstamp=`git describe --tags`" var buildstamp = "undef" // CredValidator holds additional config params for a credential validator. type credValidator struct { // AuthLevel(s) which require this validator. requiredAuthLvl []auth.Level addToTags bool } var globals struct { // Topics cache and processing. hub *Hub // Indicator that shutdown is in progress shuttingDown bool // Sessions cache. sessionStore *SessionStore // Cluster data. cluster *Cluster // Unique node ID used by Redis realtime fanout when native clustering is disabled. nodeID string // gRPC server. grpcServer *grpc.Server // Plugins. plugins []Plugin // Runtime statistics communication channel. statsUpdate chan *varUpdate // Users cache communication channel. usersUpdate chan *UserCacheReq // Credential validators. validators map[string]credValidator // Credential validator config to pass to clients. validatorClientConfig map[string][]string // Validators required for each auth level. authValidators map[auth.Level][]string // Salt used for signing API key. apiKeySalt []byte // Tag namespaces (prefixes) which are immutable to the client. immutableTagNS map[string]bool // Tag namespaces which are immutable on User and partially mutable on Topic: // user can only mutate tags he owns. maskedTagNS map[string]bool // Na,espace used for unique user and topic aliases. aliasTagNS string // Add Strict-Transport-Security to headers, the value signifies age. // Empty string "" turns it off tlsStrictMaxAge string // Listen for connections on this address:port and redirect them to HTTPS port. tlsRedirectHTTP string // Maximum message size allowed from peer. maxMessageSize int64 // Log complete client packets. Disabled by default because it is expensive on the publish hot path. logClientPackets bool // Maximum number of group topic subscribers. maxSubscriberCount int // Maximum number of indexable tags. maxTagCount int // If true, ordinary users cannot delete their accounts. permanentAccounts bool // Maximum allowed upload size. maxFileUploadSize int64 // Periodicity of a garbage collector for abandoned media uploads. mediaGcPeriod time.Duration // Prioritize X-Forwarded-For header as the source of IP address of the client. useXForwardedFor bool // Add X-Frame-Options header to HTTP response. xFrameOptions string // Country code to assign to sessions by default. defaultCountryCode string // Time before the call is dropped if not answered. callEstablishmentTimeout int // ICE servers config (video calling) iceServers []iceServer // Websocket per-message compression negotiation is enabled. wsCompression bool // URL of the main endpoint. // DEPRECTATED: use file-serving gRPC API instead. This feature will be removed. servingAt string // P2P auth access mode. With or without the D permission depending on P2PDeleteAge. typesModeCP2P types.AccessMode // Maximum age of messages which can be deleted with 'D' permission. msgDeleteAge time.Duration } // Credential validator config. type validatorConfig struct { // TRUE or FALSE to set AddToTags bool `json:"add_to_tags"` // Authentication level which triggers this validator: "auth", "anon"... or "" Required []string `json:"required"` // Validator params passed to validator unchanged. Config json.RawMessage `json:"config"` } // Stale unvalidated user account GC config. type accountGcConfig struct { Enabled bool `json:"enabled"` // How often to run GC (seconds). GcPeriod int `json:"gc_period"` // Number of accounts to delete in one pass. GcBlockSize int `json:"gc_block_size"` // Minimum hours since account was last modified. GcMinAccountAge int `json:"gc_min_account_age"` } // Large file handler config. type mediaConfig struct { // The name of the handler to use for file uploads. UseHandler string `json:"use_handler"` // Maximum allowed size of an uploaded file MaxFileUploadSize int64 `json:"max_size"` // Garbage collection timeout GcPeriod int `json:"gc_period"` // Number of entries to delete in one pass GcBlockSize int `json:"gc_block_size"` // Individual handler config params to pass to handlers unchanged. Handlers map[string]json.RawMessage `json:"handlers"` } func resolveMediaHandlerConfig(config *mediaConfig) (string, string, error) { if config == nil { return "", "", nil } handler := strings.TrimSpace(config.UseHandler) if handler == "sys_config" { values, err := store.ThirdPartyConfig.Get("im_third_party", "media_handler") if err != nil { return "", "", err } handler = strings.TrimSpace(values["media_handler"]) } if handler == "" { return "", "", errors.New("empty media handler") } var conf string if config.Handlers != nil { if params := config.Handlers[handler]; params != nil { conf = string(params) } } if handler == "oss" { ossConf, err := ensureOSSUsesSysConfig(conf) if err != nil { return "", "", err } return handler, ossConf, nil } return handler, conf, nil } func ensureOSSUsesSysConfig(conf string) (string, error) { params := map[string]any{} if strings.TrimSpace(conf) != "" { if err := json.Unmarshal([]byte(conf), ¶ms); err != nil { return "", err } } params["use_sys_config"] = true encoded, err := json.Marshal(params) if err != nil { return "", err } return string(encoded), nil } // Contentx of the configuration file type configType struct { // HTTP(S) address:port to listen on for websocket and long polling clients. Either a // numeric or a canonical name, e.g. ":80" or ":https". Could include a host name, e.g. // "localhost:80". // Could be blank: if TLS is not configured, will use ":80", otherwise ":443". // Can be overridden from the command line, see option --listen. Listen string `json:"listen"` // Base URL path where the streaming and large file API calls are served, default is '/'. // Can be overridden from the command line, see option --api_path. ApiPath string `json:"api_path"` // Cache-Control value for static content. CacheControl int `json:"cache_control"` // If true, do not attempt to negotiate websocket per message compression (RFC 7692.4). // It should be disabled (set to true) if you are using MSFT IIS as a reverse proxy. WSCompressionDisabled bool `json:"ws_compression_disabled"` // Address:port to listen for gRPC clients. If blank gRPC support will not be initialized. // Could be overridden from the command line with --grpc_listen. GrpcListen string `json:"grpc_listen"` // Enable handling of gRPC keepalives https://github.com/grpc/grpc/blob/master/doc/keepalive.md // This sets server's GRPC_ARG_KEEPALIVE_TIME_MS to 60 seconds instead of the default 2 hours. GrpcKeepalive bool `json:"grpc_keepalive_enabled"` // URL path for mounting the directory with static files (usually TinodeWeb). StaticMount string `json:"static_mount"` // Local path to static files. All files in this path are made accessible by HTTP. StaticData string `json:"static_data"` // Salt used in signing API keys APIKeySalt []byte `json:"api_key_salt"` // Maximum message size allowed from client. Intended to prevent malicious client from sending // very large files inband (does not affect out of band uploads). MaxMessageSize int `json:"max_message_size"` // Log every decoded client packet. Intended only for short diagnostic sessions. LogClientPackets bool `json:"log_client_packets"` // Maximum number of group topic subscribers. MaxSubscriberCount int `json:"max_subscriber_count"` // Masked tags namespaces: tags immutable on User (mask), mutable on Topic only within the mask. MaskedTagNamespaces []string `json:"masked_tags"` // Tag namespace used for unique user and topic aliases. AliasTagNamespace string `json:"alias_tag"` // Maximum number of indexable tags. MaxTagCount int `json:"max_tag_count"` // If true, ordinary users cannot delete their accounts. PermanentAccounts bool `json:"permanent_accounts"` // URL path for exposing runtime stats. Disabled if the path is blank. ExpvarPath string `json:"expvar"` // URL path for internal server status. Disabled if the path is blank. ServerStatusPath string `json:"server_status"` // Unique IM node ID for Redis realtime fanout. Defaults to IM_NODE_ID or HOSTNAME. NodeID string `json:"node_id"` // Take IP address of the client from HTTP header 'X-Forwarded-For'. // Useful when tinode is behind a proxy. If missing, fallback to default RemoteAddr. UseXForwardedFor bool `json:"use_x_forwarded_for"` // Add X-Frame-Options to HTTP response headers. It should be one of "DENY", "SAMEORIGIN", // "-" (disabled). The default is SAMEORIGIN. XFrameOptions string `json:"x_frame_options"` // 2-letter country code (ISO 3166-1 alpha-2) to assign to sessions by default // when the country isn't specified by the client explicitly and // it's impossible to infer it. DefaultCountryCode string `json:"default_country_code"` // Permit hard-deleting messages in p2p topics for both participants. // If it's set to 'false' then the message is only deleted for the peer who issued the command. // If it's 'true' then the message is deleted completely by either participant. // Changing the value affects the ability to hard-delete (the added or removed the D permission) // only for new topics going forward. P2PDeleteEnabled bool `json:"p2p_delete_enabled"` // The maximum age of a message in seconds when it can be deleted by users with the 'D' permission. // E.g. 600 means messages up to 10 minutes old can be deleted, older than that cannot be deleted. // Missing or 0 means no age limit. // Does not affect topic owners: owners can delete any message. MsgDeleteAge int `json:"msg_delete_age"` // Configs for subsystems Cluster json.RawMessage `json:"cluster_config"` Plugin json.RawMessage `json:"plugins"` Store json.RawMessage `json:"store_config"` Kafka json.RawMessage `json:"kafka"` Internal json.RawMessage `json:"internal_api"` AdminAPI json.RawMessage `json:"admin_internal_api"` Push json.RawMessage `json:"push"` TLS json.RawMessage `json:"tls"` Auth map[string]json.RawMessage `json:"auth_config"` Validator map[string]*validatorConfig `json:"acc_validation"` AccountGC *accountGcConfig `json:"acc_gc_config"` Media *mediaConfig `json:"media"` WebRTC json.RawMessage `json:"webrtc"` } func main() { executable, _ := os.Executable() logFlags := flag.String("log_flags", "stdFlags", "Comma-separated list of log flags (as defined in https://golang.org/pkg/log/#pkg-constants without the L prefix)") configfile := flag.String("config", "meetingim.conf", "Path to config file.") // Path to static content. staticPath := flag.String("static_data", defaultStaticPath, "File path to directory with static files to be served.") listenOn := flag.String("listen", "", "Override address and port to listen on for HTTP(S) clients.") apiPath := flag.String("api_path", "", "Override the base URL path where API is served.") listenGrpc := flag.String("grpc_listen", "", "Override address and port to listen on for gRPC clients.") tlsEnabled := flag.Bool("tls_enabled", false, "Override config value for enabling TLS.") clusterSelf := flag.String("cluster_self", "", "Override the name of the current cluster node.") expvarPath := flag.String("expvar", "", "Override the URL path where runtime stats are exposed. Use '-' to disable.") serverStatusPath := flag.String("server_status", "", "Override the URL path where the server's internal status is displayed. Use '-' to disable.") pprofFile := flag.String("pprof", "", "File name to save profiling info to. Disabled if not set.") pprofUrl := flag.String("pprof_url", "", "Debugging only! URL path for exposing profiling info. Disabled if not set.") flag.Parse() logs.Init(os.Stderr, *logFlags) curwd, err := os.Getwd() if err != nil { logs.Err.Fatal("Couldn't get current working directory: ", err) } logs.Info.Printf("Server v%s:%s:%s; pid %d; %d process(es)", currentVersion, executable, buildstamp, os.Getpid(), runtime.GOMAXPROCS(runtime.NumCPU())) *configfile = toAbsolutePath(curwd, *configfile) logs.Info.Printf("Using config from '%s'", *configfile) var config configType if file, err := os.Open(*configfile); err != nil { logs.Err.Fatal("Failed to read config file: ", err) } else { jr := jcr.New(file) if err = json.NewDecoder(jr).Decode(&config); err != nil { switch jerr := err.(type) { case *json.UnmarshalTypeError: lnum, cnum, _ := jr.LineAndChar(jerr.Offset) logs.Err.Fatalf("Unmarshall error in config file in %s at %d:%d (offset %d bytes): %s", jerr.Field, lnum, cnum, jerr.Offset, jerr.Error()) case *json.SyntaxError: lnum, cnum, _ := jr.LineAndChar(jerr.Offset) logs.Err.Fatalf("Syntax error in config file at %d:%d (offset %d bytes): %s", lnum, cnum, jerr.Offset, jerr.Error()) default: logs.Err.Fatal("Failed to parse config file: ", err) } } file.Close() } if *listenOn != "" { config.Listen = *listenOn } // Set up HTTP server. Must use non-default mux because of expvar. mux := http.NewServeMux() // Exposing values for statistics and monitoring. evpath := *expvarPath if evpath == "" { evpath = config.ExpvarPath } statsInit(mux, evpath) statsRegisterInt("Version") decVersion := base10Version(parseVersion(buildstamp)) if decVersion <= 0 { decVersion = base10Version(parseVersion(currentVersion)) } statsSet("Version", decVersion) // Initialize serving debug profiles (optional). servePprof(mux, *pprofUrl) // Initialize cluster and receive calculated workerId. // Cluster won't be started here yet. workerId := clusterInit(config.Cluster, clusterSelf) globals.nodeID = resolveIMNodeID(config.NodeID) if *pprofFile != "" { *pprofFile = toAbsolutePath(curwd, *pprofFile) cpuf, err := os.Create(*pprofFile + ".cpu") if err != nil { logs.Err.Fatal("Failed to create CPU pprof file: ", err) } defer cpuf.Close() memf, err := os.Create(*pprofFile + ".mem") if err != nil { logs.Err.Fatal("Failed to create Mem pprof file: ", err) } defer memf.Close() pprof.StartCPUProfile(cpuf) defer pprof.StopCPUProfile() defer pprof.WriteHeapProfile(memf) logs.Info.Printf("Profiling info saved to '%s.(cpu|mem)'", *pprofFile) } adminInternalClient, err := initAdminInternalAPI(config.AdminAPI) if err != nil { logs.Err.Fatal("Failed to initialize Admin internal API client:", err) } if storeConfig, ok, err := storeConfigWithAdminUIDKey(config.Store, adminInternalClient); err != nil { logs.Warn.Printf("Failed to load UID key from Admin internal config, falling back to store_config.uid_key: %v", err) } else if ok { config.Store = storeConfig logs.Info.Println("Loaded store UID key from Admin internal config") } err = store.Store.Open(workerId, config.Store) logs.Info.Println("DB adapter", store.Store.GetAdapterName(), store.Store.GetAdapterVersion()) if err != nil { logs.Err.Fatal("Failed to connect to DB: ", err) } defer func() { store.Store.Close() logs.Info.Println("Closed database connection(s)") logs.Info.Println("All done, good bye") }() statsRegisterDbStats() if kafkaConfigured(config.Kafka) { tenants, tenantErr := store.Tenants.ListActive(10000) if tenantErr != nil { logs.Err.Fatal("Failed to list tenants for Kafka message schema validation:", tenantErr) } tenantIDs := make([]types.TenantID, 0, len(tenants)) for i := range tenants { tenantIDs = append(tenantIDs, tenants[i].ID) } validator, ok := store.Store.GetAdapter().(adapter.TenantMessageSchemaAdapter) if !ok { logs.Err.Fatal("Kafka message persistence requires a message schema validator") } if err = validator.TenantMessageSchemaValidate(tenantIDs); err != nil { logs.Err.Fatal("Kafka message persistence schema validation failed:", err) } } if err = initKafkaRuntime(config.Kafka); err != nil { logs.Err.Fatal("Failed to initialize Kafka:", err) } defer stopKafkaRuntime() if err = initInternalAPI(config.Internal); err != nil { logs.Err.Fatal("Failed to initialize internal API:", err) } // API key signing secret globals.apiKeySalt = config.APIKeySalt err = store.InitAuthLogicalNames(config.Auth["logical_names"]) if err != nil { logs.Err.Fatal(err) } // List of tag namespaces for user discovery which cannot be changed directly // by the client, e.g. 'email' or 'tel'. globals.immutableTagNS = make(map[string]bool) authNames := store.Store.GetAuthNames() for _, name := range authNames { if authhdl := store.Store.GetLogicalAuthHandler(name); authhdl == nil { logs.Err.Fatalln("Unknown authenticator", name) } else if jsconf := config.Auth[authhdl.GetRealName()]; jsconf != nil { if err := authhdl.Init(jsconf, name); err != nil { logs.Err.Fatalln("Failed to init auth scheme", name+":", err) } tags, err := authhdl.RestrictedTags() if err != nil { logs.Err.Fatalln("Failed get restricted tag namespaces (prefixes)", name+":", err) } for _, tag := range tags { if strings.Contains(tag, ":") { logs.Err.Fatalln("tags restricted by auth handler should not contain character ':'", tag) } globals.immutableTagNS[tag] = true } } } // Process validators. for name, vconf := range config.Validator { // Check if validator is restrictive. If so, add validator name to the list of restricted tags. // The namespace can be restricted even if the validator is disabled. if vconf.AddToTags { if strings.Contains(name, ":") { logs.Err.Fatalln("acc_validation names should not contain character ':'", name) } globals.immutableTagNS[name] = true } if len(vconf.Required) == 0 && name != "tel" { // Optional product phone registration uses the tel validator even // when it is not required for an auth level. Other validators stay // disabled when they are not explicitly required. continue } var reqLevels []auth.Level for _, req := range vconf.Required { lvl := auth.ParseAuthLevel(req) if lvl == auth.LevelNone { logs.Err.Fatalf("Invalid required AuthLevel '%s' in validator '%s'", req, name) } reqLevels = append(reqLevels, lvl) if globals.authValidators == nil { globals.authValidators = make(map[auth.Level][]string) } globals.authValidators[lvl] = append(globals.authValidators[lvl], name) } if val := store.Store.GetValidator(name); val == nil { logs.Err.Fatal("Config provided for an unknown validator '" + name + "'") } else if err = val.Init(string(vconf.Config)); err != nil { logs.Err.Fatal("Failed to init validator '"+name+"': ", err) } if globals.validators == nil { globals.validators = make(map[string]credValidator) } globals.validators[name] = credValidator{ requiredAuthLvl: reqLevels, addToTags: vconf.AddToTags, } } // Create credential validator config for clients. if len(globals.authValidators) > 0 { globals.validatorClientConfig = make(map[string][]string) for key, val := range globals.authValidators { globals.validatorClientConfig[key.String()] = val } } // Partially restricted tag namespaces. globals.maskedTagNS = make(map[string]bool, len(config.MaskedTagNamespaces)) for _, tag := range config.MaskedTagNamespaces { if strings.Contains(tag, ":") { logs.Err.Fatal("masked_tags namespaces should not contain character ':'", tag) } globals.maskedTagNS[tag] = true } // Alias namespace. config.AliasTagNamespace = strings.TrimSpace(config.AliasTagNamespace) if config.AliasTagNamespace != "" { if prefix, _ := validateTag(config.AliasTagNamespace + ":testing"); prefix == "" { logs.Err.Fatal("alias_tag namespace should contain only alphanumeric characters and '_'", config.AliasTagNamespace) } globals.aliasTagNS = config.AliasTagNamespace } var tags []string for tag := range globals.immutableTagNS { tags = append(tags, "'"+tag+"'") } if len(tags) > 0 { logs.Info.Println("Restricted tags:", tags) } tags = nil for tag := range globals.maskedTagNS { tags = append(tags, "'"+tag+"'") } if len(tags) > 0 { logs.Info.Println("Masked tags:", tags) } if len(globals.aliasTagNS) > 0 { logs.Info.Println("Alias tag:", globals.aliasTagNS) } // Maximum message size globals.maxMessageSize = int64(config.MaxMessageSize) if globals.maxMessageSize <= 0 { globals.maxMessageSize = defaultMaxMessageSize } globals.logClientPackets = config.LogClientPackets // Maximum number of group topic subscribers globals.maxSubscriberCount = config.MaxSubscriberCount if globals.maxSubscriberCount <= 1 { globals.maxSubscriberCount = defaultMaxSubscriberCount } // Maximum number of indexable tags per user or topics globals.maxTagCount = config.MaxTagCount if globals.maxTagCount <= 0 { globals.maxTagCount = defaultMaxTagCount } // If account deletion is disabled. globals.permanentAccounts = config.PermanentAccounts globals.useXForwardedFor = config.UseXForwardedFor globals.defaultCountryCode = config.DefaultCountryCode if globals.defaultCountryCode == "" { globals.defaultCountryCode = defaultCountryCode } // Default access mode for P2P: with/without the D permission. globals.typesModeCP2P = types.ModeCP2P if config.P2PDeleteEnabled { globals.typesModeCP2P = types.ModeCP2PD } if config.MsgDeleteAge > 0 { globals.msgDeleteAge = time.Duration(config.MsgDeleteAge) * time.Second } // Configuration of X-Frame-Options header. globals.xFrameOptions = config.XFrameOptions if globals.xFrameOptions == "" { globals.xFrameOptions = "SAMEORIGIN" } if globals.xFrameOptions != "SAMEORIGIN" && globals.xFrameOptions != "DENY" && globals.xFrameOptions != "-" { logs.Warn.Println("Ignored invalid x_frame_options", config.XFrameOptions) globals.xFrameOptions = "SAMEORIGIN" } // Websocket compression. globals.wsCompression = !config.WSCompressionDisabled if config.Media != nil { if config.Media.UseHandler == "" { config.Media = nil } else { globals.maxFileUploadSize = config.Media.MaxFileUploadSize if config.Media.Handlers != nil { handler, conf, err := resolveMediaHandlerConfig(config.Media) if err != nil { logs.Err.Fatalf("Failed to resolve media handler config: %s", err) } config.Media.UseHandler = handler if err = store.Store.UseMediaHandler(handler, conf); err != nil { logs.Err.Fatalf("Failed to init media handler '%s': %s", handler, err) } } if config.Media.GcPeriod > 0 && config.Media.GcBlockSize > 0 { globals.mediaGcPeriod = time.Second * time.Duration(config.Media.GcPeriod) stopFilesGc := largeFileRunGarbageCollection(globals.mediaGcPeriod, config.Media.GcBlockSize) defer func() { stopFilesGc <- true logs.Info.Println("Stopped files garbage collector") }() } } } // Stale unvalidated user account garbage collection. if config.AccountGC != nil && config.AccountGC.Enabled { if config.AccountGC.GcPeriod <= 0 || config.AccountGC.GcBlockSize <= 0 || config.AccountGC.GcMinAccountAge <= 0 { logs.Err.Fatalln("Invalid account GC config") } gcPeriod := time.Second * time.Duration(config.AccountGC.GcPeriod) stopAccountGc := garbageCollectUsers(gcPeriod, config.AccountGC.GcBlockSize, config.AccountGC.GcMinAccountAge) defer func() { stopAccountGc <- true logs.Info.Println("Stopped account garbage collector") }() } pushHandlers, err := push.Init(config.Push) if err != nil { logs.Err.Fatal("Failed to initialize push notifications:", err) } defer func() { push.Stop() logs.Info.Println("Stopped push notifications") }() logs.Info.Println("Push handlers configured:", pushHandlers) if err = initVideoCalls(config.WebRTC); err != nil { logs.Err.Fatal("Failed to init video calls: %w", err) } // Keep inactive LP sessions for 15 seconds globals.sessionStore = NewSessionStore(idleSessionTimeout + 15*time.Second) // The hub (the main message router) globals.hub = newHub() startRedisRealtimeFanout(context.Background()) startKafkaAdminCommandConsumer() startKafkaConfigEventConsumer() startKafkaCacheInvalidateConsumer() startKafkaWorkbenchEventConsumer() startKafkaMessagePersistConsumer() startKafkaDeadLetterConsumer() // Start accepting cluster traffic. if globals.cluster != nil { globals.cluster.start() } tlsConfig, err := parseTLSConfig(*tlsEnabled, config.TLS) if err != nil { logs.Err.Fatalln(err) } // Initialize plugins. pluginsInit(config.Plugin) // Initialize users cache usersInit() // Set up gRPC server, if one is configured if *listenGrpc == "" { *listenGrpc = config.GrpcListen } if globals.grpcServer, err = serveGrpc(*listenGrpc, config.GrpcKeepalive, tlsConfig); err != nil { logs.Err.Fatal(err) } // Serve static content from the directory in -static_data flag if that's // available, otherwise assume '/static'. The content is served at // the path pointed by 'static_mount' in the config. If that is missing then it's // served at root '/'. var staticMountPoint string if *staticPath != "" && *staticPath != "-" { // Resolve path to static content. *staticPath = toAbsolutePath(curwd, *staticPath) if _, err = os.Stat(*staticPath); os.IsNotExist(err) { logs.Err.Fatal("Static content directory is not found", *staticPath) } staticMountPoint = config.StaticMount if staticMountPoint == "" { staticMountPoint = defaultStaticMount } else { if !strings.HasPrefix(staticMountPoint, "/") { staticMountPoint = "/" + staticMountPoint } if !strings.HasSuffix(staticMountPoint, "/") { staticMountPoint += "/" } } mux.Handle(staticMountPoint, // Add optional Cache-Control header. cacheControlHandler(config.CacheControl, // Optionally add Strict-Transport-Security and X-Frame-Options to the response. optionalHttpHeaders( // Add gzip compression. gh.CompressHandler( // And add custom formatter of errors. httpErrorHandler( // Remove mount point prefix. http.StripPrefix(staticMountPoint, http.FileServer(http.Dir(*staticPath)))))))) logs.Info.Printf("Serving static content from '%s' at '%s'", *staticPath, staticMountPoint) } else { logs.Info.Println("Static content is disabled") } // Configure root path for serving API calls. if *apiPath != "" { config.ApiPath = *apiPath } if config.ApiPath == "" { config.ApiPath = defaultApiPath } else { if !strings.HasPrefix(config.ApiPath, "/") { config.ApiPath = "/" + config.ApiPath } if !strings.HasSuffix(config.ApiPath, "/") { config.ApiPath += "/" } } logs.Info.Printf("API served from root URL path '%s'", config.ApiPath) // Best guess location of the main endpoint. // TODO: provide fix for the case when the serving is over unix sockets. // TODO: implement serving large files over gRPC, then remove globals.servingAt. globals.servingAt = config.Listen + config.ApiPath if tlsConfig != nil { globals.servingAt = "https://" + globals.servingAt } else { globals.servingAt = "http://" + globals.servingAt } sspath := *serverStatusPath if sspath == "" { sspath = config.ServerStatusPath } if sspath != "" && sspath != "-" { logs.Info.Printf("Server status is available at '%s'", sspath) mux.HandleFunc(sspath, serveStatus) } mux.HandleFunc("/healthz", serveHealthz) mux.HandleFunc("/livez", serveLivez) mux.HandleFunc("/metrics", serveMetrics) // Public legal pages are opened directly by clients before login. mux.HandleFunc("/legal/user-terms", serveLegalDocument) mux.HandleFunc("/legal/privacy-policy", serveLegalDocument) // Handle websocket clients. mux.HandleFunc(config.ApiPath+"v0/channels", serveWebSocket) // Product HTTP API. mux.HandleFunc(config.ApiPath+"v1/im/tenants/resolve", serveIMTenantResolve) mux.HandleFunc(config.ApiPath+"v1/im/public/uploads", serveIMPublicUploads) mux.HandleFunc(config.ApiPath+"v1/im/tenant-applications", serveIMTenantApplications) mux.HandleFunc(config.ApiPath+"v1/im/tenant-entry-feedback", serveIMTenantEntryFeedback) mux.HandleFunc(config.ApiPath+"v1/im/auth/verification-codes", serveIMVerificationCodes) mux.HandleFunc(config.ApiPath+"v1/im/auth/verification-codes/", serveIMVerificationCodeVerify) mux.HandleFunc(config.ApiPath+"v1/im/auth/password-reset/challenges/", serveIMPasswordResetVerify) mux.HandleFunc(config.ApiPath+"v1/im/auth/password-reset/confirm", serveIMPasswordResetConfirm) mux.HandleFunc(config.ApiPath+"v1/im/auth/logout", serveIMAuthLogout) mux.HandleFunc(config.ApiPath+"v1/im/security/urls/check", serveIMSecurityURLCheck) mux.HandleFunc(config.ApiPath+"v1/im/security/risk-challenges/", serveIMRiskChallengeConfirm) mux.HandleFunc(config.ApiPath+"v1/im/security/challenges", serveIMSecurityChallenges) mux.HandleFunc(config.ApiPath+"v1/im/security/challenges/", serveIMSecurityChallengeVerify) mux.HandleFunc(config.ApiPath+"v1/im/security/realname", serveIMRealName) mux.HandleFunc(config.ApiPath+"v1/im/security/realname/appeals", serveIMRealNameAppeals) mux.HandleFunc(config.ApiPath+"v1/im/security/realname/submissions", serveIMRealNameSubmissions) mux.HandleFunc(config.ApiPath+"v1/im/bootstrap", serveIMBootstrap) mux.HandleFunc(config.ApiPath+"v1/im/reports/abuse/reasons", serveIMAbuseReasons) mux.HandleFunc(config.ApiPath+"v1/im/reports/abuse", serveIMAbuseReports) mux.HandleFunc(config.ApiPath+"v1/im/feedback/config", serveIMFeedbackConfig) mux.HandleFunc(config.ApiPath+"v1/im/feedback", serveIMFeedback) mux.HandleFunc(config.ApiPath+"v1/im/contact-us", serveIMContactUs) mux.HandleFunc(config.ApiPath+"v1/im/app/releases/latest", serveIMAppReleaseLatest) mux.HandleFunc(config.ApiPath+"v1/im/me", serveIMMe) mux.HandleFunc(config.ApiPath+"v1/im/me/profile", serveIMMeProfile) mux.HandleFunc(config.ApiPath+"v1/im/me/privacy", serveIMMePrivacy) mux.HandleFunc(config.ApiPath+"v1/im/me/password", serveIMMePasswordChange) mux.HandleFunc(config.ApiPath+"v1/im/me/deletion", serveIMMeDeletion) mux.HandleFunc(config.ApiPath+"v1/im/settings/youth-mode/enable", serveIMYouthModeEnable) mux.HandleFunc(config.ApiPath+"v1/im/settings/youth-mode/disable", serveIMYouthModeDisable) mux.HandleFunc(config.ApiPath+"v1/im/settings/youth-mode", serveIMYouthMode) mux.HandleFunc(config.ApiPath+"v1/im/settings/independent-password", serveIMIndependentPassword) mux.HandleFunc(config.ApiPath+"v1/im/settings/independent-password/verify", serveIMIndependentPasswordVerify) mux.HandleFunc(config.ApiPath+"v1/im/devices/", serveIMDevicePushToken) mux.HandleFunc(config.ApiPath+"v1/im/search/conversations", serveIMConversationSearch) mux.HandleFunc(config.ApiPath+"v1/im/conversations/p2p", serveIMP2PConversation) mux.HandleFunc(config.ApiPath+"v1/im/forward-targets", serveIMForwardTargets) mux.HandleFunc(config.ApiPath+"v1/im/messages/forward", serveIMMessageForward) mux.HandleFunc(config.ApiPath+"v1/im/system-notifications", serveIMSystemNotifications) mux.HandleFunc(config.ApiPath+"v1/im/system-notifications/", serveIMSystemNotificationRead) mux.HandleFunc(config.ApiPath+"v1/im/favorites", serveIMFavorites) mux.HandleFunc(config.ApiPath+"v1/im/favorites/", serveIMFavoriteResource) mux.HandleFunc(config.ApiPath+"v1/im/calls/rtc-join", serveIMCallRTCJoin) mux.HandleFunc(config.ApiPath+"v1/im/calls/", serveIMCallsByTopic) mux.HandleFunc(config.ApiPath+"v1/im/calls", serveIMCalls) mux.HandleFunc(config.ApiPath+"v1/im/topics/", serveIMTopicMessageAction) mux.HandleFunc(config.ApiPath+"v1/im/conversations/", serveIMConversationContext) mux.HandleFunc(config.ApiPath+"v1/im/group-invitations/", serveIMGroupInvitationResource) mux.HandleFunc(config.ApiPath+"v1/im/groups", serveIMGroups) mux.HandleFunc(config.ApiPath+"v1/im/groups/", serveIMGroupResource) mux.HandleFunc(config.ApiPath+"v1/im/me/qrcodes", serveIMMeQRCodes) mux.HandleFunc(config.ApiPath+"v1/im/qrcodes/resolve", serveIMQRCodesResolve) mux.HandleFunc(config.ApiPath+"v1/im/users/search", serveIMUserSearch) mux.HandleFunc(config.ApiPath+"v1/im/users/", serveIMUserResource) mux.HandleFunc(config.ApiPath+"v1/im/friend-requests", serveIMFriendRequests) mux.HandleFunc(config.ApiPath+"v1/im/friend-requests/", serveIMFriendRequestAction) mux.HandleFunc(config.ApiPath+"v1/im/blocks", serveIMBlocks) mux.HandleFunc(config.ApiPath+"v1/im/blocks/", serveIMBlockUser) mux.HandleFunc(config.ApiPath+"v1/im/friends", serveIMFriends) mux.HandleFunc(config.ApiPath+"v1/im/friends/", serveIMFriendResource) adminQueryPrefix := config.ApiPath + "internal/admin-query/" mux.HandleFunc(adminQueryPrefix, func(wrt http.ResponseWriter, req *http.Request) { serveInternalAdminQuery(wrt, req, adminQueryPrefix) }) cultivatePrefix := config.ApiPath + "internal/cultivate/" mux.HandleFunc(cultivatePrefix, func(wrt http.ResponseWriter, req *http.Request) { serveInternalCultivate(wrt, req, cultivatePrefix) }) // Handle long polling clients. Enable compression. mux.Handle(config.ApiPath+"v0/channels/lp", gh.CompressHandler(http.HandlerFunc(serveLongPoll))) if config.Media != nil { // Handle uploads of large files. mux.Handle(config.ApiPath+"v0/file/u/", gh.CompressHandler(http.HandlerFunc(largeFileReceiveHTTP))) // Serve large files. mux.Handle(config.ApiPath+"v0/file/s/", gh.CompressHandler(http.HandlerFunc(largeFileServeHTTP))) logs.Info.Println("Large media handling enabled", config.Media.UseHandler) } if staticMountPoint != "/" { // Serve json-formatted 404 for all other URLs mux.HandleFunc("/", serve404) } if err = listenAndServe(config.Listen, monitorHTTP(withIMHTTPProtocol(config.ApiPath+"v1/im/", mux)), tlsConfig, signalHandler()); err != nil { logs.Err.Fatal(err) } }