mg-transport-core/core/engine.go

374 lines
9.1 KiB
Go
Raw Normal View History

2019-09-04 15:22:27 +03:00
package core
import (
"crypto/x509"
"fmt"
"html/template"
2021-07-29 11:29:31 +03:00
"io/fs"
2019-10-18 13:18:36 +03:00
"net/http"
2019-12-26 16:26:38 +03:00
"sync"
"github.com/getsentry/sentry-go"
2019-09-04 15:22:27 +03:00
"github.com/gin-gonic/gin"
2019-10-31 14:21:39 +03:00
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
2019-09-18 13:40:36 +03:00
"github.com/op/go-logging"
"golang.org/x/text/language"
"github.com/retailcrm/mg-transport-core/v2/core/config"
"github.com/retailcrm/mg-transport-core/v2/core/db"
"github.com/retailcrm/mg-transport-core/v2/core/middleware"
"github.com/retailcrm/mg-transport-core/v2/core/util"
"github.com/retailcrm/mg-transport-core/v2/core/util/httputil"
"github.com/retailcrm/mg-transport-core/v2/core/logger"
2019-09-04 15:22:27 +03:00
)
var boolTrue = true
// DefaultHTTPClientConfig is a default config for HTTP client. It will be used by Engine for building HTTP client
// if HTTP client config is not present in the configuration.
var DefaultHTTPClientConfig = &config.HTTPClientConfig{
Timeout: 30,
SSLVerification: &boolTrue,
}
// AppInfo contains information about app version.
type AppInfo struct {
Version string
Commit string
Build string
BuildDate string
}
// Release information for Sentry.
func (a AppInfo) Release() string {
if a.Version == "" {
a.Version = "<unknown version>"
}
if a.Build == "" {
a.Build = "<unknown build>"
}
if a.BuildDate == "" {
a.BuildDate = "<unknown build date>"
}
if a.Commit == "" {
a.Commit = "<no commit info>"
}
return fmt.Sprintf("%s (%s, built %s, commit \"%s\")", a.Version, a.Build, a.BuildDate, a.Commit)
}
// Engine struct.
2019-09-04 15:22:27 +03:00
type Engine struct {
logger logger.Logger
AppInfo AppInfo
Sessions sessions.Store
LogFormatter logging.Formatter
Config config.Configuration
ginEngine *gin.Engine
csrf *middleware.CSRF
httpClient *http.Client
jobManager *JobManager
db.ORM
2019-09-04 15:22:27 +03:00
Localizer
util.Utils
PreloadLanguages []language.Tag
Sentry
mutex sync.RWMutex
prepared bool
2019-09-04 15:22:27 +03:00
}
// New Engine instance (must be configured manually, gin can be accessed via engine.Router() directly or
// engine.ConfigureRouter(...) with callback).
func New(appInfo AppInfo) *Engine {
2019-09-04 15:22:27 +03:00
return &Engine{
Config: nil,
AppInfo: appInfo,
Localizer: Localizer{
i18nStorage: &sync.Map{},
loadMutex: &sync.RWMutex{},
},
PreloadLanguages: []language.Tag{},
ORM: db.ORM{},
Sentry: Sentry{},
Utils: util.Utils{},
ginEngine: nil,
logger: nil,
mutex: sync.RWMutex{},
prepared: false,
2019-09-04 15:22:27 +03:00
}
}
func (e *Engine) initGin() {
2019-09-19 14:16:52 +03:00
if !e.Config.IsDebug() {
2019-09-04 15:22:27 +03:00
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
e.buildSentryConfig()
e.InitSentrySDK()
r.Use(e.SentryMiddlewares()...)
2019-09-04 15:22:27 +03:00
2019-09-19 14:16:52 +03:00
if e.Config.IsDebug() {
2019-09-04 15:22:27 +03:00
r.Use(gin.Logger())
}
r.Use(e.LocalizationMiddleware())
2019-09-04 15:22:27 +03:00
e.ginEngine = r
}
// Prepare engine for start.
2019-09-04 15:22:27 +03:00
func (e *Engine) Prepare() *Engine {
if e.prepared {
panic("engine already initialized")
}
if e.Config == nil {
panic("engine.Config must be loaded before initializing")
}
if e.DefaultError == "" {
e.DefaultError = "error"
}
if e.LogFormatter == nil {
e.LogFormatter = logger.DefaultLogFormatter()
2019-09-04 15:22:27 +03:00
}
if e.LocaleMatcher == nil {
e.LocaleMatcher = DefaultLocalizerMatcher()
}
if e.isUnd(e.Localizer.LanguageTag) {
e.Localizer.LanguageTag = DefaultLanguage
}
2019-09-04 15:22:27 +03:00
e.LoadTranslations()
if len(e.PreloadLanguages) > 0 {
e.Localizer.Preload(e.PreloadLanguages)
}
e.CreateDB(e.Config.GetDBConfig())
e.ResetUtils(e.Config.GetAWSConfig(), e.Config.IsDebug(), 0)
e.SetLogger(logger.NewStandard(e.Config.GetTransportInfo().GetCode(), e.Config.GetLogLevel(), e.LogFormatter))
2019-09-04 15:22:27 +03:00
e.Sentry.Localizer = &e.Localizer
2019-12-26 16:26:38 +03:00
e.Utils.Logger = e.Logger()
e.Sentry.Logger = e.Logger()
e.buildSentryConfig()
e.Sentry.InitSentrySDK()
2019-09-04 15:22:27 +03:00
e.prepared = true
return e
}
// TemplateFuncMap combines func map for templates.
func (e *Engine) TemplateFuncMap(functions template.FuncMap) template.FuncMap {
funcMap := e.LocalizationFuncMap()
for name, fn := range functions {
funcMap[name] = fn
}
2019-12-12 09:35:05 +03:00
funcMap["version"] = func() string {
return e.Config.GetVersion()
}
return funcMap
}
// CreateRenderer with translation function.
func (e *Engine) CreateRenderer(callback func(*Renderer), funcs template.FuncMap) Renderer {
renderer := NewRenderer(e.TemplateFuncMap(funcs))
2019-09-04 15:22:27 +03:00
callback(&renderer)
return renderer
}
2021-07-29 11:29:31 +03:00
// CreateRendererFS with translation function and embedded files.
func (e *Engine) CreateRendererFS(
templatesFS fs.FS,
callback func(*Renderer),
funcs template.FuncMap,
) Renderer {
2019-09-18 10:28:55 +03:00
renderer := NewRenderer(e.TemplateFuncMap(funcs))
2021-07-29 11:29:31 +03:00
renderer.TemplatesFS = templatesFS
2019-09-18 10:28:55 +03:00
callback(&renderer)
return renderer
}
// Router will return current gin.Engine or panic if it's not present.
2019-09-04 15:22:27 +03:00
func (e *Engine) Router() *gin.Engine {
if !e.prepared {
panic("prepare engine first")
}
if e.ginEngine == nil {
e.initGin()
}
return e.ginEngine
}
// JobManager will return singleton JobManager from Engine.
func (e *Engine) JobManager() *JobManager {
if e.jobManager == nil {
2019-12-26 16:26:38 +03:00
e.jobManager = NewJobManager().SetLogger(e.Logger()).SetLogging(e.Config.IsDebug())
}
return e.jobManager
}
// Logger returns current logger.
func (e *Engine) Logger() logger.Logger {
2019-12-26 16:26:38 +03:00
return e.logger
}
// SetLogger sets provided logger instance to engine.
func (e *Engine) SetLogger(l logger.Logger) *Engine {
2019-12-26 16:26:38 +03:00
if l == nil {
return e
}
e.mutex.Lock()
defer e.mutex.Unlock()
e.logger = l
return e
}
// BuildHTTPClient builds HTTP client with provided configuration.
func (e *Engine) BuildHTTPClient(certs *x509.CertPool, replaceDefault ...bool) *Engine {
client, err := httputil.NewHTTPClientBuilder().
WithLogger(e.Logger()).
SetLogging(e.Config.IsDebug()).
SetCertPool(certs).
FromConfig(e.GetHTTPClientConfig()).
Build(replaceDefault...)
if err != nil {
panic(err)
} else {
e.httpClient = client
2019-10-18 13:18:36 +03:00
}
return e
}
// GetHTTPClientConfig returns configuration for HTTP client.
func (e *Engine) GetHTTPClientConfig() *config.HTTPClientConfig {
if e.Config.GetHTTPClientConfig() != nil {
return e.Config.GetHTTPClientConfig()
}
return DefaultHTTPClientConfig
}
// SetHTTPClient sets HTTP client to engine.
func (e *Engine) SetHTTPClient(client *http.Client) *Engine {
if client != nil {
e.httpClient = client
}
return e
}
// HTTPClient returns inner http client or default http client.
2019-10-18 13:18:36 +03:00
func (e *Engine) HTTPClient() *http.Client {
if e.httpClient == nil {
return http.DefaultClient
}
2019-12-12 09:35:05 +03:00
return e.httpClient
2019-10-18 13:18:36 +03:00
}
2019-10-31 14:21:39 +03:00
// WithCookieSessions generates new CookieStore with optional key length.
// Default key length is 32 bytes.
func (e *Engine) WithCookieSessions(keyLength ...int) *Engine {
length := 32
if len(keyLength) > 0 && keyLength[0] > 0 {
length = keyLength[0]
}
e.Sessions = sessions.NewCookieStore(securecookie.GenerateRandomKey(length))
return e
}
2019-12-12 09:35:05 +03:00
// WithFilesystemSessions generates new FilesystemStore with optional key length.
2019-10-31 14:21:39 +03:00
// Default key length is 32 bytes.
func (e *Engine) WithFilesystemSessions(path string, keyLength ...int) *Engine {
length := 32
if len(keyLength) > 0 && keyLength[0] > 0 {
length = keyLength[0]
}
e.Sessions = sessions.NewFilesystemStore(path, securecookie.GenerateRandomKey(length))
return e
}
// InitCSRF initializes CSRF middleware. engine.Sessions must be already initialized,
// use engine.WithCookieStore or engine.WithFilesystemStore for that.
// Syntax is similar to core.NewCSRF, but you shouldn't pass sessionName, store and salt.
func (e *Engine) InitCSRF(
secret string, abortFunc middleware.CSRFAbortFunc, getter middleware.CSRFTokenGetter) *Engine {
2019-10-31 14:21:39 +03:00
if e.Sessions == nil {
panic("engine.Sessions must be initialized first")
}
e.csrf = middleware.NewCSRF("", secret, "", e.Sessions, abortFunc, getter)
2019-10-31 14:21:39 +03:00
return e
}
// VerifyCSRFMiddleware returns CSRF verifier middleware
// Usage:
// engine.Router().Use(engine.VerifyCSRFMiddleware(core.DefaultIgnoredMethods))
func (e *Engine) VerifyCSRFMiddleware(ignoredMethods []string) gin.HandlerFunc {
if e.csrf == nil {
panic("csrf is not initialized")
}
return e.csrf.VerifyCSRFMiddleware(ignoredMethods)
}
// GenerateCSRFMiddleware returns CSRF generator middleware
// Usage:
// engine.Router().Use(engine.GenerateCSRFMiddleware())
func (e *Engine) GenerateCSRFMiddleware() gin.HandlerFunc {
if e.csrf == nil {
panic("csrf is not initialized")
}
return e.csrf.GenerateCSRFMiddleware()
}
// GetCSRFToken returns CSRF token from provided context.
2019-10-31 14:21:39 +03:00
func (e *Engine) GetCSRFToken(c *gin.Context) string {
if e.csrf == nil {
panic("csrf is not initialized")
}
return e.csrf.CSRFFromContext(c)
}
// ConfigureRouter will call provided callback with current gin.Engine, or panic if engine is not present.
2019-09-04 15:22:27 +03:00
func (e *Engine) ConfigureRouter(callback func(*gin.Engine)) *Engine {
callback(e.Router())
return e
}
// Run gin.Engine loop, or panic if engine is not present.
2019-09-04 15:22:27 +03:00
func (e *Engine) Run() error {
return e.Router().Run(e.Config.GetHTTPConfig().Listen)
}
// buildSentryConfig from app configuration.
func (e *Engine) buildSentryConfig() {
if e.AppInfo.Version == "" {
e.AppInfo.Version = e.Config.GetVersion()
}
e.SentryConfig = sentry.ClientOptions{
Dsn: e.Config.GetSentryDSN(),
ServerName: e.Config.GetHTTPConfig().Host,
Release: e.AppInfo.Release(),
AttachStacktrace: true,
Debug: e.Config.IsDebug(),
}
}