mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
* failing to find i18n shouldn't segfault The server was trying to handle the fact that it couldn't find the i18n directory, by emitting a translated log message... * fix utils.FindDir The attempts to find the directory in the parent or grandparent directory don't work if the current working directory was inside `enterprise`, with `enterprise` itself being a symlink as per the usual developer setup. Recurse to the root of the filesystem, cleaning the path along the way to work around this limitation (and allow tests to be run from an arbitrarily deep nesting level.) Fix corresponding usages to employ filepath.Join. * failing to find html templates shouldn't segfault * fail fast if the test user cannot be created * rework utils.FindDir to retain backwards compatibility
493 lines
13 KiB
Go
493 lines
13 KiB
Go
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
|
// See License.txt for license information.
|
|
|
|
package app
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"html/template"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
l4g "github.com/alecthomas/log4go"
|
|
"github.com/gorilla/mux"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/mattermost/mattermost-server/einterfaces"
|
|
ejobs "github.com/mattermost/mattermost-server/einterfaces/jobs"
|
|
"github.com/mattermost/mattermost-server/jobs"
|
|
"github.com/mattermost/mattermost-server/model"
|
|
"github.com/mattermost/mattermost-server/plugin/pluginenv"
|
|
"github.com/mattermost/mattermost-server/store"
|
|
"github.com/mattermost/mattermost-server/store/sqlstore"
|
|
"github.com/mattermost/mattermost-server/utils"
|
|
)
|
|
|
|
type App struct {
|
|
goroutineCount int32
|
|
goroutineExitSignal chan struct{}
|
|
|
|
Srv *Server
|
|
|
|
PluginEnv *pluginenv.Environment
|
|
PluginConfigListenerId string
|
|
|
|
EmailBatching *EmailBatchingJob
|
|
|
|
Hubs []*Hub
|
|
HubsStopCheckingForDeadlock chan bool
|
|
|
|
Jobs *jobs.JobServer
|
|
|
|
AccountMigration einterfaces.AccountMigrationInterface
|
|
Brand einterfaces.BrandInterface
|
|
Cluster einterfaces.ClusterInterface
|
|
Compliance einterfaces.ComplianceInterface
|
|
DataRetention einterfaces.DataRetentionInterface
|
|
Elasticsearch einterfaces.ElasticsearchInterface
|
|
Emoji einterfaces.EmojiInterface
|
|
Ldap einterfaces.LdapInterface
|
|
MessageExport einterfaces.MessageExportInterface
|
|
Metrics einterfaces.MetricsInterface
|
|
Mfa einterfaces.MfaInterface
|
|
Saml einterfaces.SamlInterface
|
|
|
|
config atomic.Value
|
|
configFile string
|
|
configListeners map[string]func(*model.Config, *model.Config)
|
|
|
|
licenseValue atomic.Value
|
|
clientLicenseValue atomic.Value
|
|
licenseListeners map[string]func()
|
|
|
|
siteURL string
|
|
|
|
newStore func() store.Store
|
|
|
|
htmlTemplateWatcher *utils.HTMLTemplateWatcher
|
|
sessionCache *utils.Cache
|
|
roles map[string]*model.Role
|
|
configListenerId string
|
|
licenseListenerId string
|
|
disableConfigWatch bool
|
|
configWatcher *utils.ConfigWatcher
|
|
asymmetricSigningKey *ecdsa.PrivateKey
|
|
|
|
pluginCommands []*PluginCommand
|
|
pluginCommandsLock sync.RWMutex
|
|
|
|
clientConfig map[string]string
|
|
clientConfigHash string
|
|
diagnosticId string
|
|
}
|
|
|
|
var appCount = 0
|
|
|
|
// New creates a new App. You must call Shutdown when you're done with it.
|
|
// XXX: For now, only one at a time is allowed as some resources are still shared.
|
|
func New(options ...Option) (outApp *App, outErr error) {
|
|
appCount++
|
|
if appCount > 1 {
|
|
panic("Only one App should exist at a time. Did you forget to call Shutdown()?")
|
|
}
|
|
|
|
app := &App{
|
|
goroutineExitSignal: make(chan struct{}, 1),
|
|
Srv: &Server{
|
|
Router: mux.NewRouter(),
|
|
},
|
|
sessionCache: utils.NewLru(model.SESSION_CACHE_SIZE),
|
|
configFile: "config.json",
|
|
configListeners: make(map[string]func(*model.Config, *model.Config)),
|
|
clientConfig: make(map[string]string),
|
|
licenseListeners: map[string]func(){},
|
|
}
|
|
defer func() {
|
|
if outErr != nil {
|
|
app.Shutdown()
|
|
}
|
|
}()
|
|
|
|
for _, option := range options {
|
|
option(app)
|
|
}
|
|
|
|
if utils.T == nil {
|
|
if err := utils.TranslationsPreInit(); err != nil {
|
|
return nil, errors.Wrapf(err, "unable to load Mattermost translation files")
|
|
}
|
|
}
|
|
model.AppErrorInit(utils.T)
|
|
if err := app.LoadConfig(app.configFile); err != nil {
|
|
return nil, err
|
|
}
|
|
app.EnableConfigWatch()
|
|
if err := utils.InitTranslations(app.Config().LocalizationSettings); err != nil {
|
|
return nil, errors.Wrapf(err, "unable to load Mattermost translation files")
|
|
}
|
|
|
|
app.configListenerId = app.AddConfigListener(func(_, _ *model.Config) {
|
|
app.configOrLicenseListener()
|
|
|
|
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CONFIG_CHANGED, "", "", "", nil)
|
|
|
|
message.Add("config", app.ClientConfigWithNoAccounts())
|
|
app.Go(func() {
|
|
app.Publish(message)
|
|
})
|
|
})
|
|
app.licenseListenerId = app.AddLicenseListener(func() {
|
|
app.configOrLicenseListener()
|
|
|
|
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_LICENSE_CHANGED, "", "", "", nil)
|
|
message.Add("license", app.GetSanitizedClientLicense())
|
|
app.Go(func() {
|
|
app.Publish(message)
|
|
})
|
|
|
|
})
|
|
app.regenerateClientConfig()
|
|
app.setDefaultRolesBasedOnConfig()
|
|
|
|
l4g.Info(utils.T("api.server.new_server.init.info"))
|
|
|
|
app.initEnterprise()
|
|
|
|
if app.newStore == nil {
|
|
app.newStore = func() store.Store {
|
|
return store.NewLayeredStore(sqlstore.NewSqlSupplier(app.Config().SqlSettings, app.Metrics), app.Metrics, app.Cluster)
|
|
}
|
|
}
|
|
|
|
if htmlTemplateWatcher, err := utils.NewHTMLTemplateWatcher("templates"); err != nil {
|
|
l4g.Error(utils.T("api.api.init.parsing_templates.error"), err)
|
|
} else {
|
|
app.htmlTemplateWatcher = htmlTemplateWatcher
|
|
}
|
|
|
|
app.Srv.Store = app.newStore()
|
|
if err := app.ensureAsymmetricSigningKey(); err != nil {
|
|
return nil, errors.Wrapf(err, "unable to ensure asymmetric signing key")
|
|
}
|
|
|
|
app.initJobs()
|
|
|
|
app.initBuiltInPlugins()
|
|
app.Srv.Router.HandleFunc("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}", app.ServePluginRequest)
|
|
app.Srv.Router.HandleFunc("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}/{anything:.*}", app.ServePluginRequest)
|
|
|
|
app.Srv.Router.NotFoundHandler = http.HandlerFunc(app.Handle404)
|
|
|
|
app.Srv.WebSocketRouter = &WebSocketRouter{
|
|
app: app,
|
|
handlers: make(map[string]webSocketHandler),
|
|
}
|
|
|
|
return app, nil
|
|
}
|
|
|
|
func (a *App) configOrLicenseListener() {
|
|
a.regenerateClientConfig()
|
|
a.setDefaultRolesBasedOnConfig()
|
|
}
|
|
|
|
func (a *App) Shutdown() {
|
|
appCount--
|
|
|
|
l4g.Info(utils.T("api.server.stop_server.stopping.info"))
|
|
|
|
a.StopServer()
|
|
a.HubStop()
|
|
|
|
a.ShutDownPlugins()
|
|
a.WaitForGoroutines()
|
|
|
|
if a.Srv.Store != nil {
|
|
a.Srv.Store.Close()
|
|
}
|
|
a.Srv = nil
|
|
|
|
if a.htmlTemplateWatcher != nil {
|
|
a.htmlTemplateWatcher.Close()
|
|
}
|
|
|
|
a.RemoveConfigListener(a.configListenerId)
|
|
a.RemoveLicenseListener(a.licenseListenerId)
|
|
l4g.Info(utils.T("api.server.stop_server.stopped.info"))
|
|
|
|
a.DisableConfigWatch()
|
|
}
|
|
|
|
var accountMigrationInterface func(*App) einterfaces.AccountMigrationInterface
|
|
|
|
func RegisterAccountMigrationInterface(f func(*App) einterfaces.AccountMigrationInterface) {
|
|
accountMigrationInterface = f
|
|
}
|
|
|
|
var brandInterface func(*App) einterfaces.BrandInterface
|
|
|
|
func RegisterBrandInterface(f func(*App) einterfaces.BrandInterface) {
|
|
brandInterface = f
|
|
}
|
|
|
|
var clusterInterface func(*App) einterfaces.ClusterInterface
|
|
|
|
func RegisterClusterInterface(f func(*App) einterfaces.ClusterInterface) {
|
|
clusterInterface = f
|
|
}
|
|
|
|
var complianceInterface func(*App) einterfaces.ComplianceInterface
|
|
|
|
func RegisterComplianceInterface(f func(*App) einterfaces.ComplianceInterface) {
|
|
complianceInterface = f
|
|
}
|
|
|
|
var dataRetentionInterface func(*App) einterfaces.DataRetentionInterface
|
|
|
|
func RegisterDataRetentionInterface(f func(*App) einterfaces.DataRetentionInterface) {
|
|
dataRetentionInterface = f
|
|
}
|
|
|
|
var elasticsearchInterface func(*App) einterfaces.ElasticsearchInterface
|
|
|
|
func RegisterElasticsearchInterface(f func(*App) einterfaces.ElasticsearchInterface) {
|
|
elasticsearchInterface = f
|
|
}
|
|
|
|
var emojiInterface func(*App) einterfaces.EmojiInterface
|
|
|
|
func RegisterEmojiInterface(f func(*App) einterfaces.EmojiInterface) {
|
|
emojiInterface = f
|
|
}
|
|
|
|
var jobsDataRetentionJobInterface func(*App) ejobs.DataRetentionJobInterface
|
|
|
|
func RegisterJobsDataRetentionJobInterface(f func(*App) ejobs.DataRetentionJobInterface) {
|
|
jobsDataRetentionJobInterface = f
|
|
}
|
|
|
|
var jobsMessageExportJobInterface func(*App) ejobs.MessageExportJobInterface
|
|
|
|
func RegisterJobsMessageExportJobInterface(f func(*App) ejobs.MessageExportJobInterface) {
|
|
jobsMessageExportJobInterface = f
|
|
}
|
|
|
|
var jobsElasticsearchAggregatorInterface func(*App) ejobs.ElasticsearchAggregatorInterface
|
|
|
|
func RegisterJobsElasticsearchAggregatorInterface(f func(*App) ejobs.ElasticsearchAggregatorInterface) {
|
|
jobsElasticsearchAggregatorInterface = f
|
|
}
|
|
|
|
var jobsElasticsearchIndexerInterface func(*App) ejobs.ElasticsearchIndexerInterface
|
|
|
|
func RegisterJobsElasticsearchIndexerInterface(f func(*App) ejobs.ElasticsearchIndexerInterface) {
|
|
jobsElasticsearchIndexerInterface = f
|
|
}
|
|
|
|
var jobsLdapSyncInterface func(*App) ejobs.LdapSyncInterface
|
|
|
|
func RegisterJobsLdapSyncInterface(f func(*App) ejobs.LdapSyncInterface) {
|
|
jobsLdapSyncInterface = f
|
|
}
|
|
|
|
var ldapInterface func(*App) einterfaces.LdapInterface
|
|
|
|
func RegisterLdapInterface(f func(*App) einterfaces.LdapInterface) {
|
|
ldapInterface = f
|
|
}
|
|
|
|
var messageExportInterface func(*App) einterfaces.MessageExportInterface
|
|
|
|
func RegisterMessageExportInterface(f func(*App) einterfaces.MessageExportInterface) {
|
|
messageExportInterface = f
|
|
}
|
|
|
|
var metricsInterface func(*App) einterfaces.MetricsInterface
|
|
|
|
func RegisterMetricsInterface(f func(*App) einterfaces.MetricsInterface) {
|
|
metricsInterface = f
|
|
}
|
|
|
|
var mfaInterface func(*App) einterfaces.MfaInterface
|
|
|
|
func RegisterMfaInterface(f func(*App) einterfaces.MfaInterface) {
|
|
mfaInterface = f
|
|
}
|
|
|
|
var samlInterface func(*App) einterfaces.SamlInterface
|
|
|
|
func RegisterSamlInterface(f func(*App) einterfaces.SamlInterface) {
|
|
samlInterface = f
|
|
}
|
|
|
|
func (a *App) initEnterprise() {
|
|
if accountMigrationInterface != nil {
|
|
a.AccountMigration = accountMigrationInterface(a)
|
|
}
|
|
if brandInterface != nil {
|
|
a.Brand = brandInterface(a)
|
|
}
|
|
if clusterInterface != nil {
|
|
a.Cluster = clusterInterface(a)
|
|
}
|
|
if complianceInterface != nil {
|
|
a.Compliance = complianceInterface(a)
|
|
}
|
|
if elasticsearchInterface != nil {
|
|
a.Elasticsearch = elasticsearchInterface(a)
|
|
}
|
|
if emojiInterface != nil {
|
|
a.Emoji = emojiInterface(a)
|
|
}
|
|
if ldapInterface != nil {
|
|
a.Ldap = ldapInterface(a)
|
|
a.AddConfigListener(func(_, cfg *model.Config) {
|
|
if err := utils.ValidateLdapFilter(cfg, a.Ldap); err != nil {
|
|
panic(utils.T(err.Id))
|
|
}
|
|
})
|
|
}
|
|
if messageExportInterface != nil {
|
|
a.MessageExport = messageExportInterface(a)
|
|
}
|
|
if metricsInterface != nil {
|
|
a.Metrics = metricsInterface(a)
|
|
}
|
|
if mfaInterface != nil {
|
|
a.Mfa = mfaInterface(a)
|
|
}
|
|
if samlInterface != nil {
|
|
a.Saml = samlInterface(a)
|
|
a.AddConfigListener(func(_, cfg *model.Config) {
|
|
a.Saml.ConfigureSP()
|
|
})
|
|
}
|
|
if dataRetentionInterface != nil {
|
|
a.DataRetention = dataRetentionInterface(a)
|
|
}
|
|
}
|
|
|
|
func (a *App) initJobs() {
|
|
a.Jobs = jobs.NewJobServer(a, a.Srv.Store)
|
|
if jobsDataRetentionJobInterface != nil {
|
|
a.Jobs.DataRetentionJob = jobsDataRetentionJobInterface(a)
|
|
}
|
|
if jobsMessageExportJobInterface != nil {
|
|
a.Jobs.MessageExportJob = jobsMessageExportJobInterface(a)
|
|
}
|
|
if jobsElasticsearchAggregatorInterface != nil {
|
|
a.Jobs.ElasticsearchAggregator = jobsElasticsearchAggregatorInterface(a)
|
|
}
|
|
if jobsElasticsearchIndexerInterface != nil {
|
|
a.Jobs.ElasticsearchIndexer = jobsElasticsearchIndexerInterface(a)
|
|
}
|
|
if jobsLdapSyncInterface != nil {
|
|
a.Jobs.LdapSync = jobsLdapSyncInterface(a)
|
|
}
|
|
}
|
|
|
|
func (a *App) DiagnosticId() string {
|
|
return a.diagnosticId
|
|
}
|
|
|
|
func (a *App) SetDiagnosticId(id string) {
|
|
a.diagnosticId = id
|
|
}
|
|
|
|
func (a *App) EnsureDiagnosticId() {
|
|
if a.diagnosticId != "" {
|
|
return
|
|
}
|
|
if result := <-a.Srv.Store.System().Get(); result.Err == nil {
|
|
props := result.Data.(model.StringMap)
|
|
|
|
id := props[model.SYSTEM_DIAGNOSTIC_ID]
|
|
if len(id) == 0 {
|
|
id = model.NewId()
|
|
systemId := &model.System{Name: model.SYSTEM_DIAGNOSTIC_ID, Value: id}
|
|
<-a.Srv.Store.System().Save(systemId)
|
|
}
|
|
|
|
a.diagnosticId = id
|
|
}
|
|
}
|
|
|
|
// Go creates a goroutine, but maintains a record of it to ensure that execution completes before
|
|
// the app is destroyed.
|
|
func (a *App) Go(f func()) {
|
|
atomic.AddInt32(&a.goroutineCount, 1)
|
|
|
|
go func() {
|
|
f()
|
|
|
|
atomic.AddInt32(&a.goroutineCount, -1)
|
|
select {
|
|
case a.goroutineExitSignal <- struct{}{}:
|
|
default:
|
|
}
|
|
}()
|
|
}
|
|
|
|
// WaitForGoroutines blocks until all goroutines created by App.Go exit.
|
|
func (a *App) WaitForGoroutines() {
|
|
for atomic.LoadInt32(&a.goroutineCount) != 0 {
|
|
<-a.goroutineExitSignal
|
|
}
|
|
}
|
|
|
|
func (a *App) HTMLTemplates() *template.Template {
|
|
if a.htmlTemplateWatcher != nil {
|
|
return a.htmlTemplateWatcher.Templates()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) HTTPClient(trustURLs bool) *http.Client {
|
|
insecure := a.Config().ServiceSettings.EnableInsecureOutgoingConnections != nil && *a.Config().ServiceSettings.EnableInsecureOutgoingConnections
|
|
|
|
if trustURLs {
|
|
return utils.NewHTTPClient(insecure, nil, nil)
|
|
}
|
|
|
|
allowHost := func(host string) bool {
|
|
if a.Config().ServiceSettings.AllowedUntrustedInternalConnections == nil {
|
|
return false
|
|
}
|
|
for _, allowed := range strings.Fields(*a.Config().ServiceSettings.AllowedUntrustedInternalConnections) {
|
|
if host == allowed {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
allowIP := func(ip net.IP) bool {
|
|
if !utils.IsReservedIP(ip) {
|
|
return true
|
|
}
|
|
if a.Config().ServiceSettings.AllowedUntrustedInternalConnections == nil {
|
|
return false
|
|
}
|
|
for _, allowed := range strings.Fields(*a.Config().ServiceSettings.AllowedUntrustedInternalConnections) {
|
|
if _, ipRange, err := net.ParseCIDR(allowed); err == nil && ipRange.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
return utils.NewHTTPClient(insecure, allowHost, allowIP)
|
|
}
|
|
|
|
func (a *App) Handle404(w http.ResponseWriter, r *http.Request) {
|
|
err := model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound)
|
|
|
|
l4g.Debug("%v: code=404 ip=%v", r.URL.Path, utils.GetIpAddress(r))
|
|
|
|
utils.RenderWebAppError(w, r, err, a.AsymmetricSigningKey())
|
|
}
|