diff --git a/.gitignore b/.gitignore index 25f6745..bc89a0b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ frontend/node_modules/ frontend/dist/ dist/ test.db-journal +logs/ diff --git a/frontend/frontend.go b/frontend/frontend.go index 8d7606e..a1b3ad2 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -7,4 +7,4 @@ import ( //go:generate npm run build //go:embed dist -var FrontEndFiles embed.FS \ No newline at end of file +var FrontEndFiles embed.FS diff --git a/go.mod b/go.mod index 107edf6..f572737 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/spf13/viper v1.12.0 golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 + gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) require ( diff --git a/go.sum b/go.sum index cfc9740..fbe5aff 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= @@ -1231,6 +1232,8 @@ gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:a gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/app/apps.go b/internal/app/apps.go index 930bc69..45010b4 100644 --- a/internal/app/apps.go +++ b/internal/app/apps.go @@ -2,36 +2,37 @@ package app import ( "errors" + "context" + "github.com/Fishwaldo/mouthpiece/internal/db" + "github.com/Fishwaldo/mouthpiece/internal/errors" + "github.com/Fishwaldo/mouthpiece/internal/filter" . "github.com/Fishwaldo/mouthpiece/internal/log" "github.com/Fishwaldo/mouthpiece/internal/message" "github.com/Fishwaldo/mouthpiece/internal/user" - "github.com/Fishwaldo/mouthpiece/internal/filter" - "github.com/Fishwaldo/mouthpiece/internal/db" - "github.com/Fishwaldo/mouthpiece/internal/errors" "github.com/jinzhu/copier" "gorm.io/gorm" ) type AppDetails struct { - AppName string `doc:"Application Name" pattern:"^[a-z0-9]+$" gorm:"unique;uniqueIndex"` - Status string `doc:"Status of Application" enum:"Enabled,Disabled" default:"Enabled"` + AppName string `doc:"Application Name" pattern:"^[a-z0-9]+$" gorm:"unique;uniqueIndex"` + Status string `doc:"Status of Application" enum:"Enabled,Disabled" default:"Enabled"` Description string `doc:"Description of Application"` - Icon string `doc:"Icon of Application"` - URL string `doc:"URL of Application"` + Icon string `doc:"Icon of Application"` + URL string `doc:"URL of Application"` } type ApplicationFilters struct { - gorm.Model `json:"-"` - AppID uint `json:"-"` - Name string + gorm.Model `json:"-"` + AppID uint `json:"-"` + Name string } type App struct { - gorm.Model `json:"-"` + gorm.Model `json:"-"` AppDetails AssociatedUsers []*user.User `gorm:"many2many:app_user;"` - Filters []ApplicationFilters + Filters []ApplicationFilters } func InitializeApps() { @@ -39,50 +40,50 @@ func InitializeApps() { db.Db.AutoMigrate(&ApplicationFilters{}) } -func GetApps() []App { +func GetApps(ctx context.Context) []App { var apps []App - db.Db.Preload("AssociatedUsers").Preload("AssociatedUsers.TransportConfigs").Preload("Filters").Find(&apps) + db.Db.WithContext(ctx).Preload("AssociatedUsers").Preload("AssociatedUsers.TransportConfigs").Preload("Filters").Find(&apps) return apps } -func FindApp(app_name string) (app *App, err error) { - tx := db.Db.Debug().Preload("AssociatedUsers").Preload("AssociatedUsers.TransportConfigs").Preload("Filters").First(&app, "app_name = ?", app_name) +func FindApp(ctx context.Context, app_name string) (app *App, err error) { + tx := db.Db.WithContext(ctx).Preload("AssociatedUsers").Preload("AssociatedUsers.TransportConfigs").Preload("Filters").First(&app, "app_name = ?", app_name) Log.V(1).Info("Finding App", "App", app_name, "Result", tx, "app", app) return app, tx.Error } -func AppExists(app_name string) (bool) { +func AppExists(ctx context.Context, app_name string) bool { var app App - tx := db.Db.First(&app, "app_name = ?", app_name) + tx := db.Db.WithContext(ctx).First(&app, "app_name = ?", app_name) return tx.Error == nil } -func CreateApp(app AppDetails) (newapp *App, err error) { - newapp, err = FindApp(app.AppName); +func CreateApp(ctx context.Context, app AppDetails) (newapp *App, err error) { + newapp, err = FindApp(ctx, app.AppName) if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { Log.Info("Creating New App", "App", app) var dbApp App copier.Copy(&dbApp, &app) - adminuser, _ := user.GetUser("admin@example.com") + adminuser, _ := user.GetUser(ctx, "admin@example.com") dbApp.AssociatedUsers = append(dbApp.AssociatedUsers, adminuser) - normaluser, _ := user.GetUser("user@example.com") + normaluser, _ := user.GetUser(ctx, "user@example.com") dbApp.AssociatedUsers = append(dbApp.AssociatedUsers, normaluser) - if (filter.FindFilter("CopyShortMessage") != nil) { + if filter.FindFilter("CopyShortMessage") != nil { dbApp.Filters = append(dbApp.Filters, ApplicationFilters{Name: "CopyShortMessage"}) } - if (filter.FindFilter("FindSeverity") != nil) { + if filter.FindFilter("FindSeverity") != nil { dbApp.Filters = append(dbApp.Filters, ApplicationFilters{Name: "FindSeverity"}) } - result := db.Db.Create(&dbApp) + result := db.Db.WithContext(ctx).Create(&dbApp) if result.Error != nil { return newapp, result.Error } - return FindApp(app.AppName) + return FindApp(ctx, app.AppName) } Log.Error(err, "App Already Exists", "App", newapp) return newapp, mperror.ErrAppExists } -func (app App) ProcessMessage(msg *msg.Message) (error) { +func (app App) ProcessMessage(ctx context.Context, msg *msg.Message) error { Log.V(1).Info("App Processing Message", "App", app.AppName, "MessageID", msg.ID) /* populate Message Fields with App Data */ msg.Body.Fields["app_description"] = app.Description @@ -90,10 +91,10 @@ func (app App) ProcessMessage(msg *msg.Message) (error) { msg.Body.Fields["app_url"] = app.URL for _, appfilter := range app.Filters { flt := filter.FindFilter(appfilter.Name) - if (flt != nil) { + if flt != nil { Log.V(1).Info("App Processing Message with Filter", "Filter", appfilter) - ok, _ := flt.ProcessMessage(msg); - if (!ok) { + ok, _ := flt.ProcessMessage(ctx, msg) + if !ok { Log.Info("App Filter Blocked Message", "App", app.AppName, "Filter", appfilter, "Message", msg) return nil } @@ -102,7 +103,7 @@ func (app App) ProcessMessage(msg *msg.Message) (error) { } } for _, user := range app.AssociatedUsers { - user.ProcessMessage(*msg) + user.ProcessMessage(ctx, *msg) } return nil -} \ No newline at end of file +} diff --git a/internal/app/restapi.go b/internal/app/restapi.go new file mode 100644 index 0000000..ec2355a --- /dev/null +++ b/internal/app/restapi.go @@ -0,0 +1,42 @@ +package app + +import ( + "net/http" + + "github.com/Fishwaldo/mouthpiece/internal/auth" + + "github.com/danielgtaylor/huma" + "github.com/danielgtaylor/huma/responses" +) + +func InitializeAppRestAPI(res *huma.Resource) error { + auth.AuthService.AddResourceURL("/v1/apps/", "apigroup:apps") + appapi := res.SubResource("/apps/") + + appapi.Get("get-apps", "Get A List of Applications", + responses.OK().ContentType("application/json"), + responses.OK().Headers("Set-Cookie"), + responses.OK().Model([]App{}), + ).Run(func(ctx huma.Context) { + ctx.WriteModel(http.StatusOK, GetApps(ctx)) + }) + + + appapi.Put("create-app", "Create a Application", + responses.OK().ContentType("application/json"), + responses.OK().Headers("Set-Cookie"), + responses.OK().Model(&App{}), + responses.NotAcceptable().ContentType("application/json"), + responses.NotAcceptable().Headers("Set-Cookie"), + ).Run(func(ctx huma.Context, input struct { + Body AppDetails + }) { + if app, err := CreateApp(ctx, input.Body); err != nil { + ctx.WriteError(http.StatusNotAcceptable, "Database Error", err) + } else { + ctx.WriteModel(http.StatusOK, app) + } + }) + + return nil +} diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 1ff755c..49df917 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -6,14 +6,17 @@ import ( "context" "crypto/sha1" "fmt" + "io/fs" "net/http" "os" -// "strings" + + // "strings" dbauth "github.com/Fishwaldo/mouthpiece/internal/auth/db" telegramauth "github.com/Fishwaldo/mouthpiece/internal/auth/telegram" "github.com/Fishwaldo/mouthpiece/internal/db" . "github.com/Fishwaldo/mouthpiece/internal/log" + "github.com/go-logr/logr" "github.com/spf13/viper" @@ -23,6 +26,7 @@ import ( "github.com/go-pkgz/auth/token" "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" "github.com/casbin/casbin/v2/util" //"github.com/casbin/casbin/v2/log" @@ -38,21 +42,22 @@ type Auth struct { } var AuthService *Auth +var alog *authLogger +var llog logr.Logger -type AuthLogger struct { +type authLogger struct { } -var AL *AuthLogger - -func (AL AuthLogger) Logf(format string, args ...interface{}) { - Log.WithName("Auth").Info("Authentication", "message", fmt.Sprintf(format, args...)) +func (AL authLogger) Logf(format string, args ...interface{}) { + llog.V(1).Info("Authentication", "message", fmt.Sprintf(format, args...)) } type AuthConfig struct { - CredChecker func(username string, password string) (ok bool, err error) + CredChecker func(username string, password string) (ok bool, err error) MapClaimsToUser token.ClaimsUpdFunc - Validator token.ValidatorFunc - Host string + Validator token.ValidatorFunc + Host string + ConfigDir fs.FS } func init() { @@ -95,15 +100,16 @@ func customGitHubProvider() (cred pkauth.Client, ch provider.CustomHandlerOpt) { return cred, ch } - func InitAuth(Config AuthConfig) { - AL = &AuthLogger{} + llog = Log.WithName("Auth") + alog = &authLogger{} + AuthService = &Auth{} var avatarcachedir string if viper.IsSet("auth.avatar.cachedir") { avatarcachedir = viper.GetString("auth.avatar.cachedir") - } else { + } else { avatarcachedir, _ = os.MkdirTemp("", "mouthpiece_avatar") } options := pkauth.Opts{ @@ -114,20 +120,20 @@ func InitAuth(Config AuthConfig) { CookieDuration: time.Hour * 24, // cookie fine to keep for long time DisableXSRF: true, // don't disable XSRF in real-life applications! Issuer: "mouthpiece", // part of token, just informational - URL: Config.Host, // base url of the protected service + URL: Config.Host, // base url of the protected service //AdminPasswd: "password", // admin password AvatarStore: avatar.NewLocalFS(avatarcachedir), // stores avatars locally - AvatarResizeLimit: 200, // resizes avatars to 200x200 - ClaimsUpd: token.ClaimsUpdFunc(Config.MapClaimsToUser), - Validator: Config.Validator, - Logger: AL, // optional logger for auth library - UseGravatar: true, // for verified provider use gravatar service + AvatarResizeLimit: 200, // resizes avatars to 200x200 + ClaimsUpd: token.ClaimsUpdFunc(Config.MapClaimsToUser), + Validator: Config.Validator, + Logger: alog, // optional logger for auth library + UseGravatar: true, // for verified provider use gravatar service } // create auth service AuthService.Service = pkauth.NewService(options) if viper.GetBool("auth.dev.enabled") { - Log.Info("Auth Dev Mode Enabled!") + llog.Info("Auth Dev Mode Enabled!") AuthService.Service.AddProvider("dev", "", "") // run dev/test oauth2 server on :8084 go func() { @@ -136,7 +142,7 @@ func InitAuth(Config AuthConfig) { return "admin@example.com" } if err != nil { - Log.Error(err, "[PANIC] failed to start dev oauth2 server") + llog.Error(err, "[PANIC] failed to start dev oauth2 server") } devAuthServer.Run(context.Background()) @@ -144,24 +150,24 @@ func InitAuth(Config AuthConfig) { } if viper.GetBool("auth.github.enabled") { if !viper.IsSet("auth.github.client_id") { - Log.Error(nil, "Github auth is enabled but client_id is not set") + llog.Error(nil, "Github auth is enabled but client_id is not set") } else { if !viper.IsSet("auth.github.client_secret") { - Log.Error(nil, "Github auth is enabled but client_secret is not set") + llog.Error(nil, "Github auth is enabled but client_secret is not set") } else { - Log.Info("Auth Github Enabled!") + llog.Info("Auth Github Enabled!") gcred, gch := customGitHubProvider() AuthService.Service.AddCustomProvider("github", gcred, gch) } } } if viper.GetBool("auth.microsoft.enabled") { - Log.Info("Auth Microsoft Enabled!") + llog.Info("Auth Microsoft Enabled!") AuthService.Service.AddProvider("microsoft", os.Getenv("AEXMPL_MS_APIKEY"), os.Getenv("AEXMPL_MS_APISEC")) } /* direct loging (username/password) is always handled */ dbprovider := dbauth.DirectHandler{ - L: AL, + L: alog, ProviderName: "direct", Issuer: options.Issuer, TokenService: AuthService.Service.TokenService(), @@ -171,7 +177,7 @@ func InitAuth(Config AuthConfig) { AuthService.Service.AddCustomHandler(dbprovider) if viper.GetBool("auth.email.enabled") { - Log.Info("Auth Email Enabled!") + llog.Info("Auth Email Enabled!") AuthService.Service.AddVerifProvider("email", "To confirm use {{.Token}}\nor follow http://arm64-1.dmz.dynam.ac:8888/auth/email/login?token={{.Token}}", provider.SenderFunc(func(address string, text string) error { // sender just prints token @@ -183,14 +189,14 @@ func InitAuth(Config AuthConfig) { if viper.GetBool("auth.telegram.enabled") { if viper.IsSet("auth.telegram.token") { - Log.Info("Auth Telegram Enabled!") + llog.Info("Auth Telegram Enabled!") // add telegram provider telegram := telegramauth.TelegramHandler{ ProviderName: "telegram", ErrorMsg: "❌ Invalid auth request. Please try clicking link again.", SuccessMsg: "✅ You have successfully authenticated!", Telegram: telegramauth.NewTelegramAPI(viper.GetString("auth.telegram.token"), http.DefaultClient), - L: AL, + L: alog, TokenService: AuthService.Service.TokenService(), AvatarSaver: AuthService.Service.AvatarProxy(), } @@ -198,40 +204,49 @@ func InitAuth(Config AuthConfig) { go func() { err := telegram.Run(context.Background()) if err != nil { - Log.Error(err, "[PANIC] failed to start telegram") + llog.Error(err, "[PANIC] failed to start telegram") } }() AuthService.Service.AddCustomHandler(&telegram) } else { - Log.Error(nil, "Telegram auth is enabled but token is not set") + llog.Error(nil, "Telegram auth is enabled but token is not set") } } - InitCasbin() - Log.Info("Auth service started") + InitCasbin(Config) + llog.Info("Auth service started") } -func InitCasbin() { +func InitCasbin(config AuthConfig) { cdb, err := gormadapter.NewAdapterByDB(db.Db) if err != nil { - Log.Error(err, "Failed to Setup Casbin Auth Adapter") + llog.Error(err, "Failed to Setup Casbin Auth Adapter") } - AuthService.AuthEnforcer, err = casbin.NewEnforcer("config/auth_model.conf", cdb) + casbinmodel, err := fs.ReadFile(config.ConfigDir, "config/auth_model.conf") if err != nil { - Log.Error(err, "Failed to setup Casbin") + llog.Error(err, "Failed to read casbin model") } + m, err := model.NewModelFromString(string(casbinmodel)) + if err != nil { + llog.Error(err, "Failed to parse casbin model") + } + AuthService.AuthEnforcer, err = casbin.NewEnforcer(m, cdb) + if err != nil { + llog.Error(err, "Failed to setup Casbin") + } + AuthService.AuthEnforcer.EnableLog(viper.GetBool("auth.debug")) AuthService.AuthEnforcer.EnableAutoSave(true) AuthService.AuthEnforcer.SetRoleManager(defaultrolemanager.NewRoleManager(10)) - if err := AuthService.AuthEnforcer.LoadModel(); err != nil { - Log.Error(err, "Failed to load Casbin model") - } + //if err := AuthService.AuthEnforcer.LoadModel(); err != nil { + // llog.Error(err, "Failed to load Casbin model") + //} if err := AuthService.AuthEnforcer.LoadPolicy(); err != nil { - Log.Error(err, "Failed to Load Casbin Policy") + llog.Error(err, "Failed to Load Casbin Policy") } if !AuthService.AuthEnforcer.AddNamedMatchingFunc("g2", "KeyMatch3", util.KeyMatch3) { - Log.Error(nil, "Failed to add g2 matching function") + llog.Error(nil, "Failed to add g2 matching function") } AuthService.AuthEnforcer.AddPolicy("role:admin", "apigroup:apps", "PUT") AuthService.AuthEnforcer.AddPolicy("role:user", "apigroup:apps", "GET") @@ -247,21 +262,20 @@ func InitCasbin() { // AuthService.AuthEnforcer.AddRoleForUser("admin", "role:admin") // AuthService.AuthEnforcer.AddRoleForUser("dev_user", "role:admin") - p, _ := AuthService.AuthEnforcer.GetImplicitPermissionsForUser("admin@example.com") - fmt.Printf("Admin Permissions: %+v\n", p) + AuthService.AuthEnforcer.SavePolicy() rm := AuthService.AuthEnforcer.GetPolicy() - Log.Info("Casbin Policy", "policy", rm) - Log.Info("Casbin User Roles", "Roles", AuthService.AuthEnforcer.GetGroupingPolicy()) - Log.Info("Casbin API Groups", "API Groups", AuthService.AuthEnforcer.GetNamedGroupingPolicy("g2")) + llog.Info("Casbin Policy", "policy", rm) + llog.Info("Casbin User Roles", "Roles", AuthService.AuthEnforcer.GetGroupingPolicy()) + llog.Info("Casbin API Groups", "API Groups", AuthService.AuthEnforcer.GetNamedGroupingPolicy("g2")) } func (a *Auth) AddResourceURL(url string, group string) bool { ok, err := a.AuthEnforcer.AddNamedGroupingPolicy("g2", url, group) if err != nil { - Log.Error(err, "Failed to add g2 policy", "url", url, "group", group) + llog.Error(err, "Failed to add g2 policy", "url", url, "group", group) } return ok } diff --git a/internal/auth/db/db.go b/internal/auth/db/db.go index a6ecdaa..5d6b0bc 100644 --- a/internal/auth/db/db.go +++ b/internal/auth/db/db.go @@ -1,8 +1,8 @@ package dbauth import ( - "crypto/sha1" //nolint "crypto/rand" + "crypto/sha1" //nolint "encoding/json" "fmt" "mime" @@ -13,8 +13,8 @@ import ( "github.com/golang-jwt/jwt" "github.com/go-pkgz/auth/logger" - "github.com/go-pkgz/auth/token" "github.com/go-pkgz/auth/provider" + "github.com/go-pkgz/auth/token" ) const ( @@ -22,7 +22,6 @@ const ( MaxHTTPBodySize = 1024 * 1024 ) - type ICredChecker func(user string, password string) (ok bool, err error) // DirectHandler implements non-oauth2 provider authorizing user in traditional way with storage @@ -83,8 +82,8 @@ func (p DirectHandler) LoginHandler(w http.ResponseWriter, r *http.Request) { userID := p.ProviderName + "_" + token.HashID(sha1.New(), creds.User) u := token.User{ - Name: creds.User, - ID: userID, + Name: creds.User, + ID: userID, Email: creds.User, } u, err = setAvatar(p.AvatarSaver, u, &http.Client{Timeout: 5 * time.Second}) @@ -196,4 +195,4 @@ func randToken() (string, error) { return "", fmt.Errorf("can't write randoms to sha1: %w", err) } return fmt.Sprintf("%x", s.Sum(nil)), nil -} \ No newline at end of file +} diff --git a/internal/auth/telegram/telegram.go b/internal/auth/telegram/telegram.go index 44c24ce..4b0c4e3 100644 --- a/internal/auth/telegram/telegram.go +++ b/internal/auth/telegram/telegram.go @@ -1,8 +1,8 @@ package telegramauth - import ( "context" + "crypto/rand" "crypto/sha1" "encoding/json" "fmt" @@ -13,7 +13,6 @@ import ( "sync" "sync/atomic" "time" - "crypto/rand" "github.com/go-pkgz/repeater" "github.com/go-pkgz/rest" @@ -146,7 +145,7 @@ func (th *TelegramHandler) ProcessUpdate(ctx context.Context, textUpdate string) th.requests.data = make(map[string]tgAuthRequest) } th.requests.Unlock() - + fmt.Printf("Processing update: %s\n", textUpdate) var updates telegramUpdate @@ -511,4 +510,4 @@ func (tg *tgAPI) parseError(r io.Reader, statusCode int) error { return fmt.Errorf("unexpected telegram API status code %d", statusCode) } return fmt.Errorf("unexpected telegram API status code %d, error: %q", statusCode, tgErr.Description) -} \ No newline at end of file +} diff --git a/internal/config.go b/internal/config.go index e76799f..75d80fe 100644 --- a/internal/config.go +++ b/internal/config.go @@ -5,30 +5,30 @@ import ( ) type OAuthConfig struct { - ClientID string `json:"clientid" doc:"OAuth Client ID"` + ClientID string `json:"clientid" doc:"OAuth Client ID"` } type FEConfig struct { - OAuthProviders map[string]OAuthConfig `json:"oauthproviders" doc:"Provider OAuth Config for Frontend"` + OAuthProviders map[string]OAuthConfig `json:"oauthproviders" doc:"Provider OAuth Config for Frontend"` } func GetFEConfig() (config *FEConfig) { config = &FEConfig{} config.OAuthProviders = make(map[string]OAuthConfig) - if (viper.GetBool("auth.github.enabled")) { + if viper.GetBool("auth.github.enabled") { config.OAuthProviders["github"] = OAuthConfig{ ClientID: viper.GetString("auth.github.client_id"), } } - if (viper.GetBool("auth.google.enabled")) { + if viper.GetBool("auth.google.enabled") { config.OAuthProviders["google"] = OAuthConfig{ ClientID: viper.GetString("auth.google.client_id"), } } - if (viper.GetBool("auth.dev.enabled")) { + if viper.GetBool("auth.dev.enabled") { config.OAuthProviders["dev"] = OAuthConfig{ ClientID: "123456", } } return config -} \ No newline at end of file +} diff --git a/internal/errors/errors.go b/internal/errors/errors.go index f15e3fe..dba94d2 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -5,7 +5,7 @@ import ( ) var ( - ErrAppExists = errors.New("App Already Exists") - ErrAppNotFound = errors.New("App Not Found") + ErrAppExists = errors.New("App Already Exists") + ErrAppNotFound = errors.New("App Not Found") ErrUserNotFound = errors.New("User Not Found") -) \ No newline at end of file +) diff --git a/internal/filter/filter.go b/internal/filter/filter.go index dfe0100..db0fad6 100644 --- a/internal/filter/filter.go +++ b/internal/filter/filter.go @@ -4,15 +4,16 @@ import ( "io/fs" "path/filepath" "strings" + "context" //"io/ioutil" "embed" "fmt" - . "github.com/Fishwaldo/mouthpiece/internal/log" - "github.com/Fishwaldo/mouthpiece/internal/message" "github.com/Fishwaldo/mouthpiece/internal/db" - + "github.com/Fishwaldo/mouthpiece/internal/log" + "github.com/Fishwaldo/mouthpiece/internal/message" + "github.com/go-logr/logr" "github.com/skx/evalfilter/v2" "github.com/skx/evalfilter/v2/object" "gorm.io/gorm" @@ -21,9 +22,11 @@ import ( //go:embed scripts var ScriptFiles embed.FS - type FilterType int +var llog logr.Logger + + const ( AppFilter = iota UserFilter @@ -35,14 +38,14 @@ func (ft FilterType) String() string { } type Filter struct { - gorm.Model `json:"-"` - Name string - Content string - Type FilterType - Enabled bool - script *evalfilter.Eval `gorm:"-" json:"-"` - ok bool `gorm:"-"` - processedMessage *msg.Message `gorm:"-" json:"-"` + gorm.Model `json:"-"` + Name string + Content string + Type FilterType + Enabled bool + script *evalfilter.Eval `gorm:"-" json:"-"` + ok bool `gorm:"-"` + processedMessage *msg.Message `gorm:"-" json:"-"` } var Filters []*Filter @@ -72,34 +75,35 @@ func loadScriptFiles(files []string, scripttype FilterType) { var flt *Filter tx := db.Db.Where("name = ? and type = ?", trimFileExtension(filepath.Base(script)), scripttype).First(&flt) if tx.RowsAffected == 0 { - Log.Info("Reading Filter Script from Filesystem", "type", scripttype, "filter", trimFileExtension(filepath.Base(script))) + llog.V(1).Info("Reading Filter Script from Filesystem", "type", scripttype, "filter", trimFileExtension(filepath.Base(script))) content, err := fs.ReadFile(ScriptFiles, script) if err != nil { - Log.Error(err, "Failed to read Filter Script File", "filename", script) + llog.Error(err, "Failed to read Filter Script File", "filename", script) continue } // // Create an evalfilter, with the script inside it. // flt = &Filter{ - Name: trimFileExtension(filepath.Base(script)), - Content: string(content), - Type: scripttype, + Name: trimFileExtension(filepath.Base(script)), + Content: string(content), + Type: scripttype, } } else { - Log.Info("Loading Filter Script from Databse", "type", scripttype, "filter", flt.Name) + llog.V(1).Info("Loading Filter Script from Databse", "type", scripttype, "filter", flt.Name) } if err := flt.SetupEvalFilter(); err == nil { - Log.Info("Loaded Filter Script ", "type", scripttype, "filter", flt.Name) + llog.Info("Loaded Filter Script ", "type", scripttype, "filter", flt.Name) Filters = append(Filters, flt) db.Db.Save(flt) } else { - Log.Error(err, "Failed to load Filter Script", "type", scripttype, "filter", flt.Name) + llog.Error(err, "Failed to load Filter Script", "type", scripttype, "filter", flt.Name) } } } func InitFilter() { + llog = log.Log.WithName("filter") db.Db.AutoMigrate(&Filter{}) Filters = make([]*Filter, 0) scripts := filterFiles("scripts/apps", ".scp") @@ -144,13 +148,13 @@ func (ev *Filter) fnPrintf(args []object.Object) object.Object { // Call the helper out := fmt.Sprintf(fs, fmtArgs...) - Log.Info("Filter Script Output", "filter", ev.Name, "output", out) + llog.Info("Filter Script Output", "filter", ev.Name, "output", out) return &object.Void{} } func (ev *Filter) fnPrint(args []object.Object) object.Object { for _, e := range args { - Log.Info("Filter Script Output", "filter", ev.Name, "Output", e.Inspect()) + llog.Info("Filter Script Output", "filter", ev.Name, "Output", e.Inspect()) } return &object.Void{} } @@ -171,78 +175,79 @@ func (ev *Filter) fnSetField(args []object.Object) object.Object { arg := args[0].ToInterface() - Log.Info("Setting Field Value", "filter", ev.Name, "field", fld, "value", arg) + llog.Info("Setting Field Value", "filter", ev.Name, "field", fld, "value", arg) ev.processedMessage.Body.Fields[fld] = arg return &object.Void{} } -func (ev *Filter) fnClearField(args[] object.Object) object.Object { - if (len(args) != 1) { +func (ev *Filter) fnClearField(args []object.Object) object.Object { + if len(args) != 1 { return &object.Null{} } // Type-check - if (args[0].Type() != object.STRING) { + if args[0].Type() != object.STRING { return &object.Null{} } fld := args[0].(*object.String).Value - Log.Info("Clearing Field Value", "filter", ev.Name, "field", fld) + llog.Info("Clearing Field Value", "filter", ev.Name, "field", fld) if _, ok := ev.processedMessage.Body.Fields[fld]; ok { delete(ev.processedMessage.Body.Fields, fld) } else { - Log.Info("Field Not Found", "filter", ev.Name, "field", fld) + llog.Info("Field Not Found", "filter", ev.Name, "field", fld) } return &object.Void{} } -func (ev *Filter) fnSetShortMessage(arg[] object.Object) object.Object { - if (len(arg) != 1) { +func (ev *Filter) fnSetShortMessage(arg []object.Object) object.Object { + if len(arg) != 1 { return &object.Null{} } // Type-check - if (arg[0].Type() != object.STRING) { + if arg[0].Type() != object.STRING { return &object.Null{} } msg := arg[0].(*object.String).Value - Log.Info("Setting Short Message", "filter", ev.Name, "message", msg) + llog.Info("Setting Short Message", "filter", ev.Name, "message", msg) ev.processedMessage.Body.ShortMsg = msg return &object.Void{} } -func (ev *Filter) fnSetSeverity(arg[] object.Object) object.Object { - if (len(arg) != 1) { +func (ev *Filter) fnSetSeverity(arg []object.Object) object.Object { + if len(arg) != 1 { return &object.Null{} } // Type-check - if (arg[0].Type() != object.STRING) { + if arg[0].Type() != object.STRING { return &object.Null{} } msg := arg[0].(*object.String).Value - Log.Info("Setting Severity", "filter", ev.Name, "Severity", msg) + llog.Info("Setting Severity", "filter", ev.Name, "Severity", msg) ev.processedMessage.Body.Severity = msg return &object.Void{} } -func (ev *Filter) ProcessMessage(msg *msg.Message) (bool, error) { +func (ev *Filter) ProcessMessage(ctx context.Context, msg *msg.Message) (bool, error) { if !ev.Enabled { return true, nil } defer func() { if err := recover(); err != nil { - Log.Error(err.(error), "Filter Script Error", "filter", ev.Name) + llog.Error(err.(error), "Filter Script Error", "filter", ev.Name) } }() - if (!ev.ok) { - Log.Info("Filter Script Not ready", "filter", ev.Name) + if !ev.ok { + llog.Info("Filter Script Not ready", "filter", ev.Name) return true, nil } ev.processedMessage = msg + ev.script.SetContext(ctx) ok, err := ev.script.Run(msg.Body) ev.processedMessage = nil - if err != nil { - Log.Info("Filter Run Failed", "filter", ev.Name, "result", ok, "Error", err) + if err != nil { + llog.Info("Filter Run Failed", "filter", ev.Name, "result", ok, "Error", err) return true, err } - Log.V(1).Info("Filter Run Success", "filter", ev.Name, "result", ok) + llog.V(1).Info("Filter Run Success", "filter", ev.Name, "result", ok) return ok, nil } @@ -251,7 +256,7 @@ func (ev *Filter) SetupEvalFilter() error { // Create an evaluator, with the script inside it. // ev.script = evalfilter.New(ev.Content) - Log.Info("Filter Script Content", "filter", ev.Name, "content", ev.Content) + llog.V(1).Info("Filter Script Content", "filter", ev.Name, "content", ev.Content) ev.script.AddFunction("printf", ev.fnPrintf) ev.script.AddFunction("print", ev.fnPrint) ev.script.AddFunction("setfield", ev.fnSetField) @@ -259,11 +264,11 @@ func (ev *Filter) SetupEvalFilter() error { ev.script.AddFunction("setshortmessage", ev.fnSetShortMessage) ev.script.AddFunction("setseverity", ev.fnSetSeverity) if err := ev.script.Prepare(); err != nil { - Log.Info("Compile Filter Script Failed", "filter", ev.Name, "error", err) + llog.Error(err, "Compile Filter Script Failed", "filter", ev.Name) ev.ok = false return err } - Log.Info("Compile Filter Script Success", "filter", ev.Name) + llog.V(1).Info("Compile Filter Script Success", "filter", ev.Name) ev.ok = true return nil -} \ No newline at end of file +} diff --git a/internal/health/health.go b/internal/health/health.go index add7963..17a9a52 100644 --- a/internal/health/health.go +++ b/internal/health/health.go @@ -5,19 +5,18 @@ import ( _ "fmt" "time" - - . "github.com/Fishwaldo/mouthpiece/internal/log" "github.com/Fishwaldo/mouthpiece/internal/db" - + "github.com/Fishwaldo/mouthpiece/internal/log" + "github.com/go-logr/logr" "github.com/alexliesenfeld/health" httpCheck "github.com/hellofresh/health-go/v4/checks/http" ) - +var llog logr.Logger var HealthChecker health.Checker func StartHealth() { - + llog = log.Log.WithName("health") HealthChecker = health.NewChecker( health.WithTimeout(10*time.Second), //health.WithInterceptors(interceptors.BasicLogger()), @@ -49,7 +48,7 @@ func BasicLogger() health.Interceptor { return func(ctx context.Context, name string, state health.CheckState) health.CheckState { now := time.Now() result := next(ctx, name, state) - Log.V(1).Info("processed health check request", + llog.V(1).Info("processed health check request", "check", name, "seconds", time.Now().Sub(now).Seconds(), "result", result.Status) return result } diff --git a/internal/log/log.go b/internal/log/log.go index 94db9ae..89b4e32 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -2,21 +2,81 @@ package log import ( "fmt" + "time" + "path/filepath" + "github.com/go-logr/logr" "github.com/go-logr/zapr" + "github.com/spf13/viper" "go.uber.org/zap" - "github.com/danielgtaylor/huma/middleware" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" + ) var Log logr.Logger var zapLog *zap.Logger -func InitLogger() { - zapLog, err := middleware.NewDefaultLogger() - if err != nil { - panic(fmt.Sprintf("Initilize Logging Failed (%v)?", err)) - } - Log = zapr.NewLogger(zapLog) +func init() { + viper.SetDefault("debug", false) + viper.SetDefault("log.dir", "logs") + viper.SetDefault("log.maxsize", 1) + viper.SetDefault("log.maxbackups", 3) + viper.SetDefault("log.maxage", 7) + viper.SetDefault("log.compress", true) + viper.SetDefault("log.level", "info") - Log.Info("Logging Started") +} + +func InitLogger() { + var cfg zap.Config + var lvl zapcore.Level + var err error + if lvl, err = zapcore.ParseLevel(viper.GetString("log.level")); err != nil { + panic(err) + } + if viper.GetBool("debug") { + fmt.Printf("Debug Enabled at %s level\n", viper.GetString("log.level")) + cfg = zap.NewDevelopmentConfig() + cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + cfg.OutputPaths = []string{"stdout"} + cfg.EncoderConfig.EncodeTime = iso8601UTCTimeEncoder + cfg.Level = zap.NewAtomicLevelAt(lvl) + var err error + if zapLog, err = cfg.Build(); err != nil { + panic(err) + } + } else { + fmt.Printf("Debug Disabled. Logging to file %s at %s level\n", filepath.Join(viper.GetString("log.dir"), "mouthpiece.log"), viper.GetString("log.level")) + lumberJackLogger := &lumberjack.Logger{ + Filename: filepath.Join(viper.GetString("log.dir"), "mouthpiece.log"), + MaxSize: viper.GetInt("log.maxsize"), // megabytes + MaxBackups: viper.GetInt("log.maxbackups"), + MaxAge: viper.GetInt("log.maxage"), //days + Compress: viper.GetBool("log.compress"), + } + ws := zapcore.AddSync(lumberJackLogger) + enccfg := zap.NewProductionEncoderConfig() + enccfg.EncodeTime = iso8601UTCTimeEncoder + core := zapcore.NewCore( + zapcore.NewJSONEncoder(enccfg), + ws, + zap.NewAtomicLevelAt(lvl), + ) + + zapLog = zap.New(core) + zap.ReplaceGlobals(zapLog) + } + Log = zapr.NewLogger(zapLog) + zap.RedirectStdLog(zapLog) + Log.Info("Logging Started", "level", viper.GetString("log.level")) +} + +// A UTC variation of ZapCore.ISO8601TimeEncoder with millisecond precision +func iso8601UTCTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.UTC().Format("2006-01-02T15:04:05.000Z")) +} + +func GetZapLogger() (*zap.Logger, error) { + return zapLog.Named("huma"), nil } \ No newline at end of file diff --git a/internal/message/message.go b/internal/message/message.go index 7228ec4..73301c0 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -1,11 +1,11 @@ package msg import ( - "time" - . "github.com/Fishwaldo/mouthpiece/internal/log" - "github.com/Fishwaldo/mouthpiece/internal/errors" "github.com/Fishwaldo/mouthpiece/internal/db" + "github.com/Fishwaldo/mouthpiece/internal/errors" + . "github.com/Fishwaldo/mouthpiece/internal/log" "gorm.io/gorm" + "time" ) func InitializeMessage() { @@ -15,21 +15,21 @@ func InitializeMessage() { type Message struct { gorm.Model - AppName string `path:"application" doc:"Application Name" Example:"MyApp"` - Body struct { - Message string `json:"message" doc:"Message to be Sent"` - ShortMsg string `json:"shortmessage,omitempty" doc:"Short Message to be Sent"` - Topic string `json:"topic,omitempty" doc:"Topic of Message"` - Severity string `json:"severity,omitempty" doc:"Severity of Message" default:"INFO"` - Timestamp time.Time `json:"timestamp,omitempty" doc:"Timestamp of Message"` - Fields map[string]interface{} `json:"fields,omitempty" doc:"Additional Fields" gorm:"-"` - } `json:"body" doc:"Message Body" gorm:"embedded"` + AppName string `path:"application" doc:"Application Name" Example:"MyApp"` + Body struct { + Message string `json:"message" doc:"Message to be Sent"` + ShortMsg string `json:"shortmessage,omitempty" doc:"Short Message to be Sent"` + Topic string `json:"topic,omitempty" doc:"Topic of Message"` + Severity string `json:"severity,omitempty" doc:"Severity of Message" default:"INFO"` + Timestamp time.Time `json:"timestamp,omitempty" doc:"Timestamp of Message"` + Fields map[string]interface{} `json:"fields,omitempty" doc:"Additional Fields" gorm:"-"` + } `json:"body" doc:"Message Body" gorm:"embedded"` Result *MessageResult `json:"result,omitempty" doc:"Result of Message"` } type MessageResult struct { - MessageID uint `json:"message_id" doc:"Message ID"` - Status string `json:"status" doc:"Status of Message"` + MessageID uint `json:"message_id" doc:"Message ID"` + Status string `json:"status" doc:"Status of Message"` } func (msg *Message) ProcessMessage() (err error) { @@ -43,4 +43,4 @@ func (msg *Message) ProcessMessage() (err error) { msg.Result = &MessageResult{MessageID: msg.ID, Status: "Queued"} return nil } -} \ No newline at end of file +} diff --git a/internal/middleware/accesscontrol.go b/internal/middleware/accesscontrol.go index 46a9eef..4c810e0 100644 --- a/internal/middleware/accesscontrol.go +++ b/internal/middleware/accesscontrol.go @@ -2,7 +2,7 @@ package middleware import ( "context" -// "fmt" + "strconv" "net/http" "github.com/Fishwaldo/mouthpiece/internal/auth" @@ -18,7 +18,6 @@ type Middleware struct { } -type CtxUserValue struct{} // Update user info in request context from go-pkgz/auth token.User to mouthpiece.User func (a *Middleware) Update() func(http.Handler) http.Handler { @@ -27,24 +26,26 @@ func (a *Middleware) Update() func(http.Handler) http.Handler { // call update only if user info exists, otherwise do nothing if tknuser, err := token.GetUserInfo(r); err == nil { /* find out DB User */ - if dbUser, err := user.GetUser(tknuser.Email); err != nil { + id, _ := strconv.Atoi(tknuser.ID) + ctx := huma.ContextFromRequest(w, r) + if dbUser, err := user.GetUserByID(ctx, uint(id)); err != nil { Log.Info("DBUser Not Found", "token", tknuser, "error", err) - ctx := huma.ContextFromRequest(w, r) - /* do Something */ - ctx.WriteError(http.StatusForbidden, "User not found", err) + + /* do Something */ + ctx.WriteError(http.StatusUnauthorized, "User not found", err) return } else { ok, res, err := auth.AuthService.AuthEnforcer.EnforceEx(dbUser.Email, r.URL.Path, r.Method) Log.V(1).Info("Access Control", "result", ok, "Policy", res, "Error", err) - if (!ok) { + if !ok { huma.ContextFromRequest(w, r).WriteError(http.StatusForbidden, "Access Denied", err) - return; + return } - r = r.WithContext(context.WithValue(r.Context(), CtxUserValue{}, tknuser)) + r = r.WithContext(context.WithValue(r.Context(), user.CtxUserValue{}, tknuser)) } h.ServeHTTP(w, r) - return; - } else { + return + } else { ctx := huma.ContextFromRequest(w, r) ctx.WriteError(http.StatusUnauthorized, "Access Denied") } @@ -52,4 +53,5 @@ func (a *Middleware) Update() func(http.Handler) http.Handler { return http.HandlerFunc(fn) } return f -} \ No newline at end of file +} + diff --git a/internal/router.go b/internal/router.go index a175a24..df4ed69 100644 --- a/internal/router.go +++ b/internal/router.go @@ -1,16 +1,18 @@ package mouthpiece import ( - . "github.com/Fishwaldo/mouthpiece/internal/log" - "github.com/Fishwaldo/mouthpiece/internal/errors" + "context" + "github.com/Fishwaldo/mouthpiece/internal/app" + "github.com/Fishwaldo/mouthpiece/internal/errors" + . "github.com/Fishwaldo/mouthpiece/internal/log" "github.com/Fishwaldo/mouthpiece/internal/message" ) -func RouteMessage(msg *msg.Message) { - if app, err := app.FindApp(msg.AppName); err == nil { - app.ProcessMessage(msg) +func RouteMessage(ctx context.Context, msg *msg.Message) { + if app, err := app.FindApp(ctx, msg.AppName); err == nil { + app.ProcessMessage(ctx, msg) } else { Log.Error(mperror.ErrAppNotFound, "App Not Found", "App", msg.AppName) } -} \ No newline at end of file +} diff --git a/internal/transport/stdout/stdout.go b/internal/transport/stdout/stdout.go index 9a945fd..4e33b41 100644 --- a/internal/transport/stdout/stdout.go +++ b/internal/transport/stdout/stdout.go @@ -1,19 +1,16 @@ package stdout import ( - "fmt" + "context" . "github.com/Fishwaldo/mouthpiece/internal/log" - "github.com/Fishwaldo/mouthpiece/internal/transport" "github.com/Fishwaldo/mouthpiece/internal/message" - + "github.com/Fishwaldo/mouthpiece/internal/transport" ) type StdoutTransport struct { - } - func init() { tp := NewStdoutTransport() transport.RegisterTransport(tp) @@ -27,11 +24,11 @@ func (t StdoutTransport) GetName() string { return "stdout" } -func (t StdoutTransport) SendMessage(config transport.TransportConfig, msg msg.Message) (err error) { +func (t StdoutTransport) SendMessage(ctx context.Context, config transport.TransportConfig, msg msg.Message) (err error) { fmt.Println("=========================================================") fmt.Printf("Message: %s\n", msg.Body.Message) fmt.Println("=========================================================") - transport.UpdateTransportStatus(t, msg, "sent") + transport.UpdateTransportStatus(ctx, t, msg, "sent") return nil } @@ -39,10 +36,10 @@ func (t StdoutTransport) Start() { Log.Info("Transport Started", "name", t.GetName()) } -func (t StdoutTransport) NewTransportConfig(){ -// user.TransportConfigs = append(user.TransportConfigs, mouthpiece.TransportConfig{ -// Transport: t.GetName(), -// Config: user.Username, -// }) +func (t StdoutTransport) NewTransportConfig(ctx context.Context) { + // user.TransportConfigs = append(user.TransportConfigs, mouthpiece.TransportConfig{ + // Transport: t.GetName(), + // Config: user.Username, + // }) return } diff --git a/internal/transport/telegram/telegram.go b/internal/transport/telegram/telegram.go index 49ed95c..b1d48fc 100644 --- a/internal/transport/telegram/telegram.go +++ b/internal/transport/telegram/telegram.go @@ -2,8 +2,7 @@ package telegram import ( "fmt" - // "os" - + "context" . "github.com/Fishwaldo/mouthpiece/internal/log" "github.com/Fishwaldo/mouthpiece/internal/message" @@ -16,9 +15,7 @@ import ( "github.com/mymmrac/telego/telegoutil" ) - type TelegramTransport struct { - } func init() { @@ -76,17 +73,17 @@ func (t TelegramTransport) Start() { Log.Info("Transport Started", "name", t.GetName()) } -func (t TelegramTransport) NewTransportConfig(){ -// user.TransportConfigs = append(user.TransportConfigs, mouthpiece.TransportConfig{ -// Transport: t.GetName(), -// Config: user.Username, -// }) +func (t TelegramTransport) NewTransportConfig(ctx context.Context) { + // user.TransportConfigs = append(user.TransportConfigs, mouthpiece.TransportConfig{ + // Transport: t.GetName(), + // Config: user.Username, + // }) } -func (t TelegramTransport) SendMessage(config transport.TransportConfig, msg msg.Message) (err error) { +func (t TelegramTransport) SendMessage(ctx context.Context, config transport.TransportConfig, msg msg.Message) (err error) { fmt.Println("=========================================================") fmt.Printf("Message: %s\n", msg.Body.Message) fmt.Println("=========================================================") - transport.UpdateTransportStatus(t, msg, "sent") + transport.UpdateTransportStatus(ctx, t, msg, "sent") return nil -} \ No newline at end of file +} diff --git a/internal/transport/transports.go b/internal/transport/transports.go index 57469a1..7beb064 100644 --- a/internal/transport/transports.go +++ b/internal/transport/transports.go @@ -2,25 +2,26 @@ package transport import ( "errors" + "context" + "github.com/Fishwaldo/mouthpiece/internal/db" . "github.com/Fishwaldo/mouthpiece/internal/log" "github.com/Fishwaldo/mouthpiece/internal/message" - "github.com/Fishwaldo/mouthpiece/internal/db" "gorm.io/gorm" ) type TransportConfig struct { - gorm.Model `json:"-"` - UserID uint `json:"-"` - Transport string - Config string + gorm.Model `json:"-"` + UserID uint `json:"-"` + Transport string + Config string } type ITransport interface { GetName() string Start() - SendMessage(config TransportConfig, message msg.Message) (err error) - NewTransportConfig() + SendMessage(ctx context.Context, config TransportConfig, message msg.Message) (err error) + NewTransportConfig(ctx context.Context) } var transports map[string]ITransport @@ -43,14 +44,14 @@ func StartTransports() { } } -func GetTransport(name string) (ITransport, error) { +func GetTransport(ctx context.Context, name string) (ITransport, error) { if t, ok := transports[name]; ok { return t, nil } return nil, errors.New("Transport Not Found") } -func GetTransports() []string { +func GetTransports(ctx context.Context) []string { var a []string for k := range transports { a = append(a, k) @@ -58,6 +59,6 @@ func GetTransports() []string { return a } -func UpdateTransportStatus(t ITransport, m msg.Message, status string) { +func UpdateTransportStatus(ctx context.Context, t ITransport, m msg.Message, status string) { Log.Info("Transport Status", "status", status, "MessageID", m.ID, "Transport", t.GetName()) } diff --git a/internal/user/auth.go b/internal/user/auth.go index ba1a484..1c9dba1 100644 --- a/internal/user/auth.go +++ b/internal/user/auth.go @@ -1,51 +1,69 @@ package user import ( + "context" + "fmt" + "strconv" "strings" "github.com/Fishwaldo/mouthpiece/internal/errors" . "github.com/Fishwaldo/mouthpiece/internal/log" "github.com/go-pkgz/auth/token" - ) +type CtxUserValue struct{} + + func dbAuthProvider(user, pass string) (ok bool, err error) { user = strings.TrimSpace(user) - Log.Info("Direct Login", "user", user, "pass", pass) - dbUser, err := GetUser(user) + Log.Info("Direct Login", "user", user) + dbUser, err := GetUser(context.Background(), user) Log.Info("User", "user", dbUser, "error", err) if err == mperror.ErrUserNotFound { Log.Info("User not found", "user", user) return false, nil } - if !dbUser.CheckPassword(pass) { + if !dbUser.CheckPassword(context.Background(), pass) { Log.Info("Password Invalid", "user", user) return false, nil } return true, nil } -// Called when the Tokens are created/refreshed. +// Called when the Tokens are created/refreshed. func MapClaimsToUser(claims token.Claims) token.Claims { - Log.Info("Map Claims To User", "claims", claims) -// if claims.User != nil { -// if user, err := GetUser(claims.User.Name); err != nil { -// Log.Info("User not found", "user", claims.User.Name) -// claims.User.SetBoolAttr("valid", false) -// } else { -// claims.User.SetStrAttr("backenduser", user.Username) -// claims.User.SetBoolAttr("valid", true) -// } -// } + //Log.Info("Map Claims To User", "claims", claims) + // if claims.User != nil { + // if user, err := GetUser(claims.User.Name); err != nil { + // Log.Info("User not found", "user", claims.User.Name) + // claims.User.SetBoolAttr("valid", false) + // } else { + // claims.User.SetStrAttr("backenduser", user.Username) + // claims.User.SetBoolAttr("valid", true) + // } + // } return claims } // called on every access to the API -func UserValidator(token string, claims token.Claims) (bool) { - Log.Info("User Validator", "token", token, "claims", claims) +func UserValidator(token string, claims token.Claims) bool { + //Log.Info("User Validator", "user", claims.User.Name) if claims.User != nil { - return true - } + if user, _ := GetUser(context.Background(), claims.User.Name); user != nil { + claims.User.ID = fmt.Sprintf("%d", user.ID) + return true + } + } return false +} + +func GetUserFromContext(ctx context.Context) (bool, *User) { + v := ctx.Value(CtxUserValue{}).(token.User) + if id, _ := strconv.Atoi(v.ID); id > 0 { + if user, _ := GetUserByID(ctx, uint(id)); user != nil { + return true, user + } + } + return false, nil } \ No newline at end of file diff --git a/internal/user/users.go b/internal/user/users.go index 9fa7798..e88600b 100644 --- a/internal/user/users.go +++ b/internal/user/users.go @@ -1,64 +1,64 @@ package user import ( + "context" "fmt" + "golang.org/x/crypto/bcrypt" - - . "github.com/Fishwaldo/mouthpiece/internal/log" - "github.com/Fishwaldo/mouthpiece/internal/message" - "github.com/Fishwaldo/mouthpiece/internal/transport" - "github.com/Fishwaldo/mouthpiece/internal/db" + "github.com/Fishwaldo/mouthpiece/internal/auth" - "github.com/Fishwaldo/mouthpiece/internal/errors" - + "github.com/Fishwaldo/mouthpiece/internal/db" + mperror "github.com/Fishwaldo/mouthpiece/internal/errors" + . "github.com/Fishwaldo/mouthpiece/internal/log" + msg "github.com/Fishwaldo/mouthpiece/internal/message" + "github.com/Fishwaldo/mouthpiece/internal/transport" + "github.com/go-playground/validator/v10" "gorm.io/gorm" "gorm.io/gorm/clause" - "github.com/go-playground/validator/v10" ) - type User struct { - gorm.Model `json:"-"` - ID uint `gorm:"primarykey"` - Email string `validate:"required,email"` - FirstName string `validate:"required"` - LastName string `validate:"required"` - Password string `json:"-" writeOnly:"true" validate:"required"` + gorm.Model `json:"-"` + ID uint `gorm:"primarykey"` + Email string `validate:"required,email"` + FirstName string `validate:"required"` + LastName string `validate:"required"` + Password string `json:"-" writeOnly:"true" validate:"required"` TransportConfigs []transport.TransportConfig `json:"transports,omitempty" gorm:"many2many:user_transports;" validate:"-"` } var AuthConfig auth.AuthConfig + func init() { - AuthConfig = auth.AuthConfig { - CredChecker: dbAuthProvider, + AuthConfig = auth.AuthConfig{ + CredChecker: dbAuthProvider, MapClaimsToUser: MapClaimsToUser, - Validator: UserValidator, + Validator: UserValidator, } } - -func CreateUser(user *User) error { +func CreateUser(ctx context.Context, user *User) error { validate := validator.New() if err := validate.Struct(user); err != nil { Log.Info("User Validation Error", "Error", err) - return err; + return err } - tx := db.Db.Omit("Password").Create(&user) + tx := db.Db.WithContext(ctx).Omit("Password").Create(&user) if tx.Error != nil { return tx.Error } - if dbuser, err := GetUser(user.Email); err == nil { + if dbuser, err := GetUser(ctx, user.Email); err == nil { /* Set the Users Initial Password */ - if err := dbuser.SetPassword(user.Password); err != nil { - if tx := db.Db.Delete(&dbuser); tx.Error != nil { + if err := dbuser.SetPassword(ctx, user.Password); err != nil { + if tx := db.Db.WithContext(ctx).Delete(&dbuser); tx.Error != nil { Log.Info("Error Deleting User after failed Password", "Error", tx.Error) - return err; + return err } return err } /* New Users all Start with User Role */ - if !dbuser.addUserRole("user") { + if !dbuser.addUserRole(ctx, "user") { Log.Info("Error Adding User Role", "Error", err) } return nil @@ -67,16 +67,16 @@ func CreateUser(user *User) error { } } -func (u *User) addUserRole(role string) bool { +func (u *User) addUserRole(ctx context.Context, role string) bool { _, err := auth.AuthService.AuthEnforcer.AddRoleForUser(u.Email, fmt.Sprintf("role:%s", role)) - if err != nil { + if err != nil { Log.Info("Failed to add role for user", "email", u.Email, "role", role, "error", err) return false } - return true; + return true } -func (u *User) CheckPassword(password string) bool { +func (u *User) CheckPassword(ctx context.Context, password string) bool { Log.Info("Checking Password", "email", u.Email) err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) if err != nil { @@ -86,14 +86,14 @@ func (u *User) CheckPassword(password string) bool { return true } -func (u *User) SetPassword(password string) error { +func (u *User) SetPassword(ctx context.Context, password string) error { Log.Info("Setting Password", "Email", u.Email) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if (err != nil) { + if err != nil { Log.Info("Error Generating SetPassword Hash", "Error", err) return err } - if tx := db.Db.Model(&u).Update("password", string(hashedPassword)); tx.Error != nil { + if tx := db.Db.WithContext(ctx).Model(&u).Update("password", string(hashedPassword)); tx.Error != nil { Log.Info("Error Setting Password", "Error", tx.Error) return tx.Error } @@ -105,54 +105,52 @@ func InitializeUsers() { var count int64 db.Db.Model(&User{}).Count(&count) Log.V(1).Info("Initializing Users", "count", count) - if (count == 0) { + if count == 0 { Log.Info("Creating Default Users") admin := &User{FirstName: "Admin", LastName: "User", Email: "admin@example.com", Password: "password"} - if err := CreateUser(admin); err == nil { - admin.addUserRole("admin") + if err := CreateUser(context.Background(), admin); err == nil { + admin.addUserRole(context.Background(), "admin") Log.Info("Created Default Admin admin@example.com") } - if err := CreateUser(&User{FirstName: "User", LastName: "User", Email: "user@example.com", Password: "password"}); err == nil { + if err := CreateUser(context.Background(), &User{FirstName: "User", LastName: "User", Email: "user@example.com", Password: "password"}); err == nil { Log.Info("Created Default User user@example.com") } } -} +} -func GetUsers() []User { +func GetUsers(ctx context.Context) []User { var users []User - db.Db.Find(&users) + db.Db.WithContext(ctx).Find(&users) return users } -func GetUser(email string) (user *User, err error) { - tx := db.Db.Preload(clause.Associations).First(&user, "email = ?", email) +func GetUser(ctx context.Context, email string) (user *User, err error) { + tx := db.Db.WithContext(ctx).Preload(clause.Associations).First(&user, "email = ?", email) if tx.Error == gorm.ErrRecordNotFound { return nil, mperror.ErrUserNotFound } return } -func GetUserByID(id uint) (user *User, err error) { - tx := db.Db.Preload(clause.Associations).First(&user, "ID = ?", id) +func GetUserByID(ctx context.Context, id uint) (user *User, err error) { + tx := db.Db.WithContext(ctx).Preload(clause.Associations).First(&user, "ID = ?", id) if tx.Error == gorm.ErrRecordNotFound { return nil, mperror.ErrUserNotFound } return } - - -func (u User) ProcessMessage(msg msg.Message) (err error) { +func (u User) ProcessMessage(ctx context.Context, msg msg.Message) (err error) { /* add User Fields to Message */ msg.Body.Fields["first_name"] = u.FirstName msg.Body.Fields["last_name"] = u.LastName msg.Body.Fields["email"] = u.Email Log.V(1).Info("User Processing Message", "Email", u.Email, "MessageID", msg.ID) for _, tc := range u.TransportConfigs { - t, err := transport.GetTransport(tc.Transport); + t, err := transport.GetTransport(ctx, tc.Transport) if err != nil { Log.Info("Cant find Transport", "Transport", tc.Transport) } - go t.SendMessage(tc, msg) + go t.SendMessage(ctx, tc, msg) } return } diff --git a/internal/version.go b/internal/version.go index 76bc274..61c66df 100644 --- a/internal/version.go +++ b/internal/version.go @@ -202,4 +202,4 @@ func (i *Info) CheckFontName(fontName string) bool { fmt.Fprintln(os.Stderr, "font not valid, using default") return false -} \ No newline at end of file +} diff --git a/main.go b/main.go index a17ca66..475165b 100644 --- a/main.go +++ b/main.go @@ -25,27 +25,25 @@ SOFTWARE. package main import ( - //"fmt" - // "context" + "embed" "fmt" - "net/http" "io/fs" - - // "reflect" - "strings" - // "unsafe" - "encoding/json" + "net/http" "os" - "runtime/debug" + "os/signal" + "strings" + "syscall" + "time" + "context" + "path/filepath" "github.com/Fishwaldo/mouthpiece/frontend" - _ "github.com/Fishwaldo/mouthpiece/frontend" mouthpiece "github.com/Fishwaldo/mouthpiece/internal" "github.com/Fishwaldo/mouthpiece/internal/app" "github.com/Fishwaldo/mouthpiece/internal/auth" "github.com/Fishwaldo/mouthpiece/internal/db" "github.com/Fishwaldo/mouthpiece/internal/filter" - . "github.com/Fishwaldo/mouthpiece/internal/log" + "github.com/Fishwaldo/mouthpiece/internal/log" msg "github.com/Fishwaldo/mouthpiece/internal/message" "github.com/Fishwaldo/mouthpiece/internal/middleware" "github.com/Fishwaldo/mouthpiece/internal/transport" @@ -60,11 +58,50 @@ import ( "github.com/go-chi/chi" "github.com/danielgtaylor/huma" - "github.com/danielgtaylor/huma/cli" + hmw "github.com/danielgtaylor/huma/middleware" "github.com/danielgtaylor/huma/responses" + "github.com/spf13/cobra" "github.com/spf13/viper" ) +//go:embed config +var ConfigFiles embed.FS + +type mpserver struct { + *huma.Router + root *cobra.Command + prestart []func() +} + +var Server mpserver + +func (c *mpserver) Flag(name, short, description string, defaultValue interface{}) { + viper.SetDefault(name, defaultValue) + + flags := c.root.PersistentFlags() + switch v := defaultValue.(type) { + case bool: + flags.BoolP(name, short, viper.GetBool(name), description) + case int, int16, int32, int64, uint16, uint32, uint64: + flags.IntP(name, short, viper.GetInt(name), description) + case float32, float64: + flags.Float64P(name, short, viper.GetFloat64(name), description) + default: + flags.StringP(name, short, fmt.Sprintf("%v", v), description) + } + viper.BindPFlag(name, flags.Lookup(name)) +} + +func (c *mpserver) PreStart(f func()) { + c.prestart = append(c.prestart, f) +} + +func (c *mpserver) Run() { + if err := c.root.Execute(); err != nil { + panic(err) + } +} + func init() { viper.SetDefault("frontend.path", "frontend/dist") viper.SetDefault("frontend.external", false) @@ -77,7 +114,7 @@ func fileServer(r chi.Router, path string, root http.FileSystem) { panic("FileServer does not permit URL parameters.") } - //log.Printf("[INFO] serving static files from %v", root) + //log.Log.Printf("[INFO] serving static files from %v", root) fs := http.StripPrefix(path, http.FileServer(root)) if path != "/" && path[len(path)-1] != '/' { @@ -91,22 +128,12 @@ func fileServer(r chi.Router, path string, root http.FileSystem) { }) } -func printBuildInfo() { - bi, ok := debug.ReadBuildInfo() - if !ok { - fmt.Println("Getting build info failed (not in module mode?)!") - return - } - - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - if err := enc.Encode(bi); err != nil { - panic(err) - } -} func main() { + viper.SetEnvPrefix("MP") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.AutomaticEnv() viper.SetConfigName("config") viper.SetConfigType("yaml") viper.AddConfigPath(".") @@ -135,50 +162,80 @@ func main() { fmt.Println(bi.String()) - // Create a new router & CLI with default middleware. - InitLogger() + + Server = mpserver{ + Router: huma.New(bi.Name, bi.GitVersion), + } + hmw.Defaults(Server.Router) + Server.root = &cobra.Command{ + Use: filepath.Base(os.Args[0]), + Version: bi.GitVersion, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Starting %s (%s)\n", bi.Name, bi.GitVersion) + for _, f := range Server.prestart { + f() + } + go func() { + if err := Server.Listen(fmt.Sprintf("%s:%v", viper.Get("host"), viper.Get("port"))); err != nil && err != http.ErrServerClosed { + panic(err) + } + }() + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + fmt.Println("Shutting down...") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + Server.Shutdown(ctx) + }, + } + Server.Flag("host", "", "Hostname", "0.0.0.0") + Server.Flag("port", "p", "Port", 8888) + + + log.InitLogger() db.InitializeDB() - humucli := cli.NewRouter(bi.Name, bi.GitVersion) - humucli.DisableSchemaProperty() - humucli.PreStart(transport.InitializeTransports) - humucli.PreStart(msg.InitializeMessage) - humucli.PreStart(user.InitializeUsers) - humucli.PreStart(app.InitializeApps) - humucli.PreStart(transport.StartTransports) - humucli.PreStart(filter.InitFilter) - // app.PreStart() - humucli.PreStart(healthChecker.StartHealth) - humucli.GatewayClientCredentials("mouthpiece", "/oauth2/token", nil) - humucli.GatewayAuthCode("mouthpiece2", "/oauth2/token", "/oauth2/token", nil) - humucli.GatewayBasicAuth("basic") + hmw.NewLogger = log.GetZapLogger + Server.DisableSchemaProperty() + Server.PreStart(transport.InitializeTransports) + Server.PreStart(msg.InitializeMessage) + Server.PreStart(user.InitializeUsers) + Server.PreStart(app.InitializeApps) + Server.PreStart(transport.StartTransports) + Server.PreStart(filter.InitFilter) + Server.PreStart(healthChecker.StartHealth) + Server.GatewayClientCredentials("mouthpiece", "/oauth2/token", nil) + Server.GatewayAuthCode("mouthpiece2", "/oauth2/token", "/oauth2/token", nil) + Server.GatewayBasicAuth("basic") user.AuthConfig.Host = fmt.Sprintf("http://arm64-1.dmz.dynam.ac:%v", viper.Get("Port")) + user.AuthConfig.ConfigDir = ConfigFiles auth.InitAuth(user.AuthConfig) m := auth.AuthService.Service.Middleware() p := middleware.Middleware{} authRoutes, avaRoutes := auth.AuthService.Service.Handlers() - mux := humucli.Resource("/").GetMux() + mux := Server.Resource("/").GetMux() mux.Mount("/auth", authRoutes) mux.Mount("/avatar", avaRoutes) var httpfiles http.FileSystem if viper.GetBool("frontend.external") { - Log.Info("Serving frontend from external location", "path", viper.GetString("frontend.path")) + log.Log.Info("Serving frontend from external location", "path", viper.GetString("frontend.path")) httpfiles = http.Dir(viper.GetString("frontend.path")) } else { - Log.Info("Serving frontend from Bundled Files") + log.Log.Info("Serving frontend from Bundled Files") subdir, err := fs.Sub(frontend.FrontEndFiles, "dist") if err != nil { - Log.Error(err, "Failed to get subdir") + log.Log.Error(err, "Failed to get subdir") } httpfiles = http.FS(subdir) } fileServer(mux, "/static", httpfiles) // Declare the root resource and a GET operation on it. - humucli.Resource("/health").Get("get-health", "Get Health of the Service", + Server.Resource("/health").Get("get-health", "Get Health of the Service", responses.OK().ContentType("application/json"), responses.OK().Headers("Content-Type"), responses.OK().Model(health.CheckerResult{}), @@ -194,7 +251,7 @@ func main() { ctx.WriteModel(status, test) }) - humucli.Resource("/config/frontend").Get("get-config", "Get Config of the Service", + Server.Resource("/config/frontend").Get("get-config", "Get Config of the Service", responses.OK().ContentType("application/json"), responses.OK().Headers("Content-Type"), responses.OK().Model(&mouthpiece.FEConfig{}), @@ -202,7 +259,7 @@ func main() { ctx.WriteModel(http.StatusOK, mouthpiece.GetFEConfig()) }) - v1api := humucli.Resource("/v1") + v1api := Server.Resource("/v1") v1api.Middleware(m.Trace) v1api.Middleware(p.Update()) @@ -212,10 +269,10 @@ func main() { responses.OK().Model(&msg.MessageResult{}), responses.NotFound().ContentType("application/json"), ).Run(func(ctx huma.Context, input msg.Message) { - Log.Info("Recieved Message", "message", input) - if app.AppExists(input.AppName) { + log.Log.Info("Recieved Message", "message", input) + if app.AppExists(ctx, input.AppName) { if err := input.ProcessMessage(); err == nil { - mouthpiece.RouteMessage(&input) + mouthpiece.RouteMessage(ctx, &input) ctx.WriteModel(http.StatusOK, input.Result) } else { ctx.WriteError(http.StatusInternalServerError, err.Error()) @@ -225,30 +282,11 @@ func main() { } }) - auth.AuthService.AddResourceURL("/v1/apps/", "apigroup:apps") - appapi := v1api.SubResource("/apps/") - appapi.Get("get-apps", "Get A List of Applications", - responses.OK().ContentType("application/json"), - responses.OK().Headers("Set-Cookie"), - responses.OK().Model([]app.App{}), - ).Run(func(ctx huma.Context) { - ctx.WriteModel(http.StatusOK, app.GetApps()) - }) - appapi.Put("create-app", "Create a Application", - responses.OK().ContentType("application/json"), - responses.OK().Headers("Set-Cookie"), - responses.OK().Model(&app.App{}), - responses.NotAcceptable().ContentType("application/json"), - responses.NotAcceptable().Headers("Set-Cookie"), - ).Run(func(ctx huma.Context, input struct { - Body app.AppDetails - }) { - if app, err := app.CreateApp(input.Body); err != nil { - ctx.WriteError(http.StatusNotAcceptable, "Database Error", err) - } else { - ctx.WriteModel(http.StatusOK, app) - } - }) + + if err := app.InitializeAppRestAPI(v1api); err != nil { + log.Log.Error(err, "Failed to initialize App Rest API") + } + auth.AuthService.AddResourceURL("/v1/users/", "apigroup:users") userapi := v1api.SubResource("/users/") @@ -257,7 +295,7 @@ func main() { responses.OK().Headers("Set-Cookie"), responses.OK().Model([]user.User{}), ).Run(func(ctx huma.Context) { - ctx.WriteModel(http.StatusOK, user.GetUsers()) + ctx.WriteModel(http.StatusOK, user.GetUsers(ctx)) }) auth.AuthService.AddResourceURL("/v1/users/{userid}/transports/", "apigroup:users") @@ -270,7 +308,7 @@ func main() { ).Run(func(ctx huma.Context, input struct { User uint `path:"userid"` }) { - if user, err := user.GetUserByID(input.User); err != nil { + if user, err := user.GetUserByID(ctx, input.User); err != nil { ctx.WriteError(http.StatusNotFound, "User Not Found", err) } else { var transport []string @@ -287,11 +325,12 @@ func main() { responses.OK().Headers("Set-Cookie"), responses.OK().Model(transport.TransportConfig{}), responses.NotFound().ContentType("application/json"), + responses.NotFound().Headers("Set-Cookie"), ).Run(func(ctx huma.Context, input struct { User uint `path:"userid"` Transport string `path:"transportid"` }) { - if user, err := user.GetUserByID(input.User); err != nil { + if user, err := user.GetUserByID(ctx, input.User); err != nil { ctx.WriteError(http.StatusNotFound, "User Not Found", err) } else { ok := false @@ -313,9 +352,9 @@ func main() { responses.OK().Headers("Set-Cookie"), responses.OK().Model([]string{}), ).Run(func(ctx huma.Context) { - ctx.WriteModel(http.StatusOK, transport.GetTransports()) + ctx.WriteModel(http.StatusOK, transport.GetTransports(ctx)) }) // Run the CLI. When passed no arguments, it starts the server. - humucli.Run() + Server.Run() }