diff --git a/Makefile b/Makefile index b42604d6..d56f38e3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION=0.79.87 +VERSION=0.79.88 BINARY_NAME=statup GOPATH:=$(GOPATH) GOCMD=go diff --git a/cmd/cli.go b/cmd/cli.go index ff8ab0f7..05211e5c 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -27,7 +27,6 @@ import ( "github.com/hunterlong/statup/utils" "github.com/joho/godotenv" "io/ioutil" - "net/http" "net/http/httptest" "time" ) @@ -35,7 +34,9 @@ import ( // catchCLI will run functions based on the commands sent to Statup func catchCLI(args []string) error { dir := utils.Directory - utils.InitLogs() + if err := utils.InitLogs(); err != nil { + return err + } source.Assets() loadDotEnvs() @@ -50,22 +51,21 @@ func catchCLI(args []string) error { } return errors.New("end") case "assets": - err := source.CreateAllAssets(dir) - if err != nil { + var err error + if err = source.CreateAllAssets(dir); err != nil { return err - } else { - return errors.New("end") } + return errors.New("end") case "sass": - err := source.CompileSASS(dir) - if err == nil { - return errors.New("end") + if err := source.CompileSASS(dir); err != nil { + return err } - return err + return errors.New("end") case "update": - gitCurrent, err := checkGithubUpdates() - if err != nil { - return nil + var err error + var gitCurrent githubResponse + if gitCurrent, err = checkGithubUpdates(); err != nil { + return err } fmt.Printf("Statup Version: v%v\nLatest Version: %v\n", VERSION, gitCurrent.TagName) if VERSION != gitCurrent.TagName[1:] { @@ -73,10 +73,7 @@ func catchCLI(args []string) error { } else { fmt.Printf("You have the latest version of Statup!\n") } - if err == nil { - return errors.New("end") - } - return nil + return errors.New("end") case "test": cmd := args[1] switch cmd { @@ -84,19 +81,17 @@ func catchCLI(args []string) error { plugin.LoadPlugins() } return errors.New("end") - case "export": + case "static": var err error fmt.Printf("Statup v%v Exporting Static 'index.html' page...\n", VERSION) utils.InitLogs() - core.Configs, err = core.LoadConfigFile(dir) - if err != nil { + if core.Configs, err = core.LoadConfigFile(dir); err != nil { utils.Log(4, "config.yml file not found") return err } indexSource := ExportIndexHTML() - core.CloseDB() - err = utils.SaveFile(dir+"/index.html", indexSource) - if err != nil { + //core.CloseDB() + if err = utils.SaveFile(dir+"/index.html", indexSource); err != nil { utils.Log(4, err) return err } @@ -104,10 +99,45 @@ func catchCLI(args []string) error { case "help": HelpEcho() return errors.New("end") + case "export": + var err error + var data []byte + if err := utils.InitLogs(); err != nil { + return err + } + if core.Configs, err = core.LoadConfigFile(dir); err != nil { + return err + } + if err = core.Configs.Connect(false, dir); err != nil { + return err + } + if data, err = core.ExportSettings(); err != nil { + return fmt.Errorf("could not export settings: %v", err.Error()) + } + //core.CloseDB() + if err = utils.SaveFile(dir+"/statup-export.json", data); err != nil { + return fmt.Errorf("could not write file statup-export.json: %v", err.Error()) + } + return errors.New("end") + case "import": + var err error + var data []byte + if len(args) != 2 { + return fmt.Errorf("did not include a JSON file to import\nstatup import filename.json") + } + filename := args[1] + if data, err = ioutil.ReadFile(filename); err != nil { + return err + } + var exportData core.ExportData + if err = json.Unmarshal(data, &exportData); err != nil { + return err + } + return errors.New("end") case "run": utils.Log(1, "Running 1 time and saving to database...") RunOnce() - core.CloseDB() + //core.CloseDB() fmt.Println("Check is complete.") return errors.New("end") case "env": @@ -175,42 +205,40 @@ func HelpEcho() { fmt.Println(" statup version - Returns the current version of Statup") fmt.Println(" statup run - Check all services 1 time and then quit") fmt.Println(" statup assets - Dump all assets used locally to be edited.") - fmt.Println(" statup export - Exports the index page as a static HTML for pushing") + fmt.Println(" statup static - Creates a static HTML file of the index page") fmt.Println(" statup sass - Compile .scss files into the css directory") fmt.Println(" statup test plugins - Test all plugins for required information") fmt.Println(" statup env - Show all environment variables being used for Statup") fmt.Println(" statup update - Attempts to update to the latest version") + fmt.Println(" statup export - Exports your Statup settings to a 'statup-export.json' file.") + fmt.Println(" statup import - Imports settings from a previously saved JSON file.") fmt.Println(" statup help - Shows the user basic information about Statup") fmt.Printf("Flags:\n") fmt.Println(" -ip 127.0.0.1 - Run HTTP server on specific IP address (default: localhost)") fmt.Println(" -port 8080 - Run HTTP server on Port (default: 8080)") fmt.Printf("Environment Variables:\n") fmt.Println(" STATUP_DIR - Set a absolute path for the root path of Statup server (logs, assets, SQL db)") - fmt.Println(" DB_CONN - Automatic Database connection (sqlite, postgres, mysql)") - fmt.Println(" DB_HOST - Database hostname or IP address") - fmt.Println(" DB_USER - Database username") - fmt.Println(" DB_PASS - Database password") - fmt.Println(" DB_PORT - Database port (5432, 3306, ...") + fmt.Println(" DB_CONN - Automatic Database connection (sqlite, postgres, mysql)") + fmt.Println(" DB_HOST - Database hostname or IP address") + fmt.Println(" DB_USER - Database username") + fmt.Println(" DB_PASS - Database password") + fmt.Println(" DB_PORT - Database port (5432, 3306, ...)") fmt.Println(" DB_DATABASE - Database connection's database name") fmt.Println(" GO_ENV - Run Statup in testmode, will bypass HTTP authentication (if set as 'true')") fmt.Println(" NAME - Set a name for the Statup status page") fmt.Println(" DESCRIPTION - Set a description for the Statup status page") - fmt.Println(" DOMAIN - Set a URL for the Statup status page") + fmt.Println(" DOMAIN - Set a URL for the Statup status page") fmt.Println(" ADMIN_USER - Username for administrator account (default: admin)") fmt.Println(" ADMIN_PASS - Password for administrator account (default: admin)") + fmt.Println(" SASS - Set the absolute path to the sass binary location") fmt.Println(" * You can insert environment variables into a '.env' file in root directory.") - fmt.Println("Give Statup a Star at https://github.com/hunterlong/statup") } func checkGithubUpdates() (githubResponse, error) { var gitResp githubResponse - response, err := http.Get("https://api.github.com/repos/hunterlong/statup/releases/latest") - if err != nil { - return githubResponse{}, err - } - defer response.Body.Close() - contents, err := ioutil.ReadAll(response.Body) + url := "https://api.github.com/repos/hunterlong/statup/releases/latest" + contents, _, err := utils.HttpRequest(url, "GET", nil, nil, nil, time.Duration(10*time.Second)) if err != nil { return githubResponse{}, err } diff --git a/cmd/cli_test.go b/cmd/cli_test.go index ec1eb001..44e71fc1 100644 --- a/cmd/cli_test.go +++ b/cmd/cli_test.go @@ -17,6 +17,7 @@ package main import ( "github.com/hunterlong/statup/core" + "github.com/hunterlong/statup/source" "github.com/hunterlong/statup/utils" "github.com/rendon/testcli" "github.com/stretchr/testify/assert" @@ -35,7 +36,6 @@ func init() { } func TestStartServerCommand(t *testing.T) { - Clean() os.Setenv("DB_CONN", "sqlite") cmd := helperCommand(nil, "") var got = make(chan string) @@ -61,9 +61,9 @@ func TestHelpCommand(t *testing.T) { } func TestExportCommand(t *testing.T) { - cmd := helperCommand(nil, "export") + cmd := helperCommand(nil, "static") var got = make(chan string) - commandAndSleep(cmd, time.Duration(4*time.Second), got) + commandAndSleep(cmd, time.Duration(10*time.Second), got) gg, _ := <-got t.Log(gg) assert.Contains(t, gg, "Exporting Static 'index.html' page...") @@ -72,10 +72,12 @@ func TestExportCommand(t *testing.T) { } func TestUpdateCommand(t *testing.T) { - c := testcli.Command("statup", "update") - c.Run() - assert.True(t, c.StdoutContains("Statup Version: "+VERSION)) - assert.True(t, c.StdoutContains("Latest Version:")) + cmd := helperCommand(nil, "version") + var got = make(chan string) + commandAndSleep(cmd, time.Duration(10*time.Second), got) + gg, _ := <-got + t.Log(gg) + assert.Contains(t, gg, "Statup") } func TestAssetsCommand(t *testing.T) { @@ -122,12 +124,12 @@ func TestSassCLI(t *testing.T) { } func TestUpdateCLI(t *testing.T) { - t.SkipNow() run := catchCLI([]string{"update"}) assert.EqualError(t, run, "end") } func TestTestPackageCLI(t *testing.T) { + t.SkipNow() run := catchCLI([]string{"test", "plugins"}) assert.EqualError(t, run, "end") } @@ -138,7 +140,6 @@ func TestHelpCLI(t *testing.T) { } func TestRunOnceCLI(t *testing.T) { - t.SkipNow() run := catchCLI([]string{"run"}) assert.EqualError(t, run, "end") } @@ -146,7 +147,6 @@ func TestRunOnceCLI(t *testing.T) { func TestEnvCLI(t *testing.T) { run := catchCLI([]string{"env"}) assert.Error(t, run) - core.CloseDB() Clean() } @@ -167,3 +167,22 @@ func runCommand(c *exec.Cmd, out chan<- string) { bout, _ := c.CombinedOutput() out <- string(bout) } + +func fileExists(file string) bool { + if _, err := os.Stat(file); os.IsNotExist(err) { + return false + } + return true +} + +func Clean() { + utils.DeleteFile(dir + "/config.yml") + utils.DeleteFile(dir + "/statup.db") + utils.DeleteDirectory(dir + "/assets") + utils.DeleteDirectory(dir + "/logs") + core.CoreApp = core.NewCore() + source.Assets() + //core.CloseDB() + os.Unsetenv("DB_CONN") + time.Sleep(2 * time.Second) +} diff --git a/cmd/main.go b/cmd/main.go index a50c4ff5..3417fa35 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -26,7 +26,6 @@ import ( "github.com/hunterlong/statup/utils" "github.com/joho/godotenv" "os" - "os/signal" ) var ( @@ -74,15 +73,6 @@ func main() { } } utils.Log(1, fmt.Sprintf("Starting Statup v%v", VERSION)) - defer core.CloseDB() - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - go func() { - <-c - core.CloseDB() - os.Exit(1) - }() core.Configs, err = core.LoadConfigFile(utils.Directory) if err != nil { diff --git a/cmd/main_test.go b/cmd/main_test.go deleted file mode 100644 index bf223004..00000000 --- a/cmd/main_test.go +++ /dev/null @@ -1,611 +0,0 @@ -// Statup -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long and the project contributors -// -// https://github.com/hunterlong/statup -// -// The licenses for most software and other practical works are designed -// to take away your freedom to share and change the works. By contrast, -// the GNU General Public License is intended to guarantee your freedom to -// share and change all versions of a program--to make sure it remains free -// software for all its users. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package main - -import ( - "github.com/gorilla/mux" - "github.com/hunterlong/statup/core" - "github.com/hunterlong/statup/core/notifier" - "github.com/hunterlong/statup/handlers" - "github.com/hunterlong/statup/source" - "github.com/hunterlong/statup/types" - "github.com/hunterlong/statup/utils" - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - "net/url" - "os" - "strings" - "testing" - "time" -) - -var ( - route *mux.Router -) - -func init() { - dir = utils.Directory -} - -func Clean() { - utils.DeleteFile(dir + "/config.yml") - utils.DeleteFile(dir + "/statup.db") - utils.DeleteDirectory(dir + "/assets") - utils.DeleteDirectory(dir + "/logs") -} - -func RunInit(db string, t *testing.T) { - Clean() - if db == "mssql" { - os.Setenv("DB_DATABASE", "tempdb") - os.Setenv("DB_PASS", "PaSsW0rD123") - os.Setenv("DB_PORT", "1433") - os.Setenv("DB_USER", "sa") - } - source.Assets() - route = handlers.Router() - core.CoreApp = core.NewCore() -} - -//func TestMain(m *testing.M) { -// m.Run() -//} - -func TestRunAll(t *testing.T) { - //t.Parallel() - - databases := []string{"sqlite", "postgres", "mysql"} - if os.Getenv("ONLY_DB") != "" { - databases = []string{os.Getenv("ONLY_DB")} - } - - for _, dbt := range databases { - t.Run(dbt+" init", func(t *testing.T) { - RunInit(dbt, t) - }) - t.Run(dbt+" Save Config", func(t *testing.T) { - RunSaveConfig(t, dbt) - }) - t.Run(dbt+" Load Configs", func(t *testing.T) { - RunLoadConfig(t) - }) - t.Run(dbt+" Connect to Database", func(t *testing.T) { - err := core.Configs.Connect(false, dir) - assert.Nil(t, err) - }) - t.Run(dbt+" Drop Database", func(t *testing.T) { - assert.NotNil(t, core.Configs) - RunDropDatabase(t) - }) - t.Run(dbt+" Connect to Database Again", func(t *testing.T) { - err := core.Configs.Connect(false, dir) - assert.Nil(t, err) - }) - t.Run(dbt+" Inserting Database Structure", func(t *testing.T) { - RunCreateSchema(t, dbt) - }) - t.Run(dbt+" Inserting Seed Data", func(t *testing.T) { - RunInsertSampleData(t) - }) - t.Run(dbt+" Connect to Database Again", func(t *testing.T) { - err := core.Configs.Connect(false, dir) - assert.Nil(t, err) - }) - t.Run(dbt+" Run Database Migrations", func(t *testing.T) { - RunDatabaseMigrations(t, dbt) - }) - t.Run(dbt+" Select Core", func(t *testing.T) { - RunSelectCoreMYQL(t, dbt) - }) - t.Run(dbt+" Select Services", func(t *testing.T) { - RunSelectAllMysqlServices(t) - }) - t.Run(dbt+" Select Comms", func(t *testing.T) { - RunSelectAllNotifiers(t) - }) - t.Run(dbt+" Create Users", func(t *testing.T) { - RunUserCreate(t) - }) - t.Run(dbt+" Update user", func(t *testing.T) { - runUserUpdate(t) - }) - t.Run(dbt+" Create Non Unique Users", func(t *testing.T) { - t.SkipNow() - runUserNonUniqueCreate(t) - }) - t.Run(dbt+" Select Users", func(t *testing.T) { - RunUserSelectAll(t) - }) - t.Run(dbt+" Select Services", func(t *testing.T) { - RunSelectAllServices(t) - }) - t.Run(dbt+" Select One Service", func(t *testing.T) { - RunOneServiceCheck(t) - }) - t.Run(dbt+" Create Service", func(t *testing.T) { - RunServiceCreate(t) - }) - t.Run(dbt+" Create Hits", func(t *testing.T) { - RunCreateServiceHits(t) - }) - t.Run(dbt+" Service ToJSON()", func(t *testing.T) { - RunServiceToJSON(t) - }) - t.Run(dbt+" Avg Time", func(t *testing.T) { - runServiceAvgTime(t) - }) - t.Run(dbt+" Online 24h", func(t *testing.T) { - RunServiceOnline24(t) - }) - //t.Run(dbt+" Chart Data", func(t *testing.T) { - // RunServiceGraphData(t) - //}) - t.Run(dbt+" Create Failing Service", func(t *testing.T) { - RunBadServiceCreate(t) - }) - t.Run(dbt+" Check Bad Service", func(t *testing.T) { - RunBadServiceCheck(t) - }) - t.Run(dbt+" Select Hits", func(t *testing.T) { - RunServiceHits(t) - }) - t.Run(dbt+" Select Failures", func(t *testing.T) { - RunServiceFailures(t) - }) - t.Run(dbt+" Select Limited Hits", func(t *testing.T) { - RunServiceLimitedHits(t) - }) - t.Run(dbt+" Delete Service", func(t *testing.T) { - RunDeleteService(t) - }) - t.Run(dbt+" Delete user", func(t *testing.T) { - RunUserDelete(t) - }) - t.Run(dbt+" HTTP /", func(t *testing.T) { - RunIndexHandler(t) - }) - t.Run(dbt+" HTTP /service/1", func(t *testing.T) { - RunServiceHandler(t) - }) - t.Run(dbt+" HTTP /metrics", func(t *testing.T) { - RunPrometheusHandler(t) - }) - t.Run(dbt+" HTTP /metrics", func(t *testing.T) { - RunFailingPrometheusHandler(t) - }) - t.Run(dbt+" HTTP /login", func(t *testing.T) { - RunLoginHandler(t) - }) - t.Run(dbt+" HTTP /dashboard", func(t *testing.T) { - RunDashboardHandler(t) - }) - t.Run(dbt+" HTTP /users", func(t *testing.T) { - RunUsersHandler(t) - }) - t.Run(dbt+" HTTP /user/1", func(t *testing.T) { - RunUserViewHandler(t) - }) - t.Run(dbt+" HTTP /services", func(t *testing.T) { - RunServicesHandler(t) - }) - t.Run(dbt+" HTTP /help", func(t *testing.T) { - RunHelpHandler(t) - }) - t.Run(dbt+" HTTP /settings", func(t *testing.T) { - RunSettingsHandler(t) - }) - t.Run(dbt+" Cleanup", func(t *testing.T) { - core.Configs.Close() - core.DbSession = nil - if dbt == "mssql" { - os.Setenv("DB_DATABASE", "root") - os.Setenv("DB_PASS", "password123") - os.Setenv("DB_PORT", "1433") - } - //Clean() - }) - - //<-done - - } - -} - -func RunSaveConfig(t *testing.T, db string) { - var err error - core.Configs = core.EnvToConfig() - core.Configs.DbConn = db - core.Configs, err = core.Configs.Save() - assert.Nil(t, err) -} - -func RunCreateSchema(t *testing.T, db string) { - err := core.Configs.Connect(false, dir) - assert.Nil(t, err) - err = core.Configs.CreateDatabase() - assert.Nil(t, err) -} - -func RunDatabaseMigrations(t *testing.T, db string) { - err := core.Configs.MigrateDatabase() - assert.Nil(t, err) -} - -func RunInsertSampleData(t *testing.T) { - err := core.InsertLargeSampleData() - assert.Nil(t, err) -} - -func RunLoadConfig(t *testing.T) { - var err error - core.Configs, err = core.LoadConfigFile(dir) - t.Log(core.Configs) - assert.Nil(t, err) - assert.NotNil(t, core.Configs) -} - -func RunDropDatabase(t *testing.T) { - err := core.Configs.DropDatabase() - assert.Nil(t, err) -} - -func RunSelectCoreMYQL(t *testing.T, db string) { - var err error - core.CoreApp, err = core.SelectCore() - if err != nil { - t.FailNow() - } - assert.Nil(t, err) - t.Log("core: ", core.CoreApp.Core) - assert.Equal(t, "Statup Sample Data", core.CoreApp.Name) - assert.Equal(t, db, core.CoreApp.DbConnection) - assert.NotEmpty(t, core.CoreApp.ApiKey) - assert.NotEmpty(t, core.CoreApp.ApiSecret) - assert.Equal(t, VERSION, core.CoreApp.Version) -} - -func RunSelectAllMysqlServices(t *testing.T) { - var err error - services, err := core.CoreApp.SelectAllServices(false) - assert.Nil(t, err) - assert.Equal(t, 15, len(services)) -} - -func RunSelectAllNotifiers(t *testing.T) { - var err error - notifier.SetDB(core.DbSession, float32(-8)) - core.CoreApp.Notifications = notifier.Load() - assert.Nil(t, err) - assert.Equal(t, 8, len(core.CoreApp.Notifications)) -} - -func RunUserSelectAll(t *testing.T) { - users, err := core.SelectAllUsers() - assert.Nil(t, err) - assert.Equal(t, 4, len(users)) -} - -func RunUserCreate(t *testing.T) { - user := core.ReturnUser(&types.User{ - Username: "hunterlong", - Password: "password123", - Email: "info@gmail.com", - Admin: types.NewNullBool(true), - }) - id, err := user.Create() - assert.Nil(t, err) - assert.Equal(t, int64(3), id) - user2 := core.ReturnUser(&types.User{ - Username: "superadmin", - Password: "admin", - Email: "info@adminer.com", - Admin: types.NewNullBool(true), - }) - id, err = user2.Create() - assert.Nil(t, err) - assert.Equal(t, int64(4), id) -} - -func runUserUpdate(t *testing.T) { - user, err := core.SelectUser(1) - user.Email = "info@updatedemail.com" - assert.Nil(t, err) - err = user.Update() - assert.Nil(t, err) - updatedUser, err := core.SelectUser(1) - assert.Nil(t, err) - assert.Equal(t, "info@updatedemail.com", updatedUser.Email) -} - -func runUserNonUniqueCreate(t *testing.T) { - user := core.ReturnUser(&types.User{ - Username: "admin", - Password: "admin", - Email: "info@testuser.com", - }) - admin, err := user.Create() - assert.Error(t, err) - assert.Nil(t, admin) -} - -func RunUserDelete(t *testing.T) { - user, err := core.SelectUser(2) - assert.Nil(t, err) - assert.NotNil(t, user) - err = user.Delete() - assert.Nil(t, err) -} - -func RunSelectAllServices(t *testing.T) { - var err error - services, err := core.CoreApp.SelectAllServices(false) - assert.Nil(t, err) - assert.Equal(t, 15, len(services)) - for _, s := range services { - assert.NotEmpty(t, s.CreatedAt) - } -} - -func RunOneServiceCheck(t *testing.T) { - service := core.SelectService(1) - assert.NotNil(t, service) - assert.Equal(t, "Google", service.Name) -} - -func RunServiceCreate(t *testing.T) { - service := core.ReturnService(&types.Service{ - Name: "test service", - Domain: "https://google.com", - ExpectedStatus: 200, - Interval: 1, - Port: 0, - Type: "http", - Method: "GET", - Timeout: 30, - }) - id, err := service.Create(false) - assert.Nil(t, err) - assert.Equal(t, int64(16), id) -} - -func RunServiceToJSON(t *testing.T) { - service := core.SelectService(1) - assert.NotNil(t, service) - jsoned := service.ToJSON() - assert.NotEmpty(t, jsoned) -} - -func runServiceAvgTime(t *testing.T) { - service := core.SelectService(1) - assert.NotNil(t, service) - avg := service.AvgUptime24() - assert.Equal(t, "100", avg) -} - -func RunServiceOnline24(t *testing.T) { - var dayAgo = time.Now().Add(-24 * time.Hour).Add(-10 * time.Minute) - - service := core.SelectService(1) - assert.NotNil(t, service) - online := service.OnlineSince(dayAgo) - assert.NotEqual(t, float32(0), online) - - service = core.SelectService(6) - assert.NotNil(t, service) - online = service.OnlineSince(dayAgo) - assert.Equal(t, float32(100), online) - - service = core.SelectService(13) - assert.NotNil(t, service) - online = service.OnlineSince(dayAgo) - assert.True(t, online > 99) - - service = core.SelectService(14) - assert.NotNil(t, service) - online = service.OnlineSince(dayAgo) - assert.True(t, online > float32(49.00)) -} - -func RunBadServiceCreate(t *testing.T) { - service := core.ReturnService(&types.Service{ - Name: "Bad Service", - Domain: "https://9839f83h72gey2g29278hd2od2d.com", - ExpectedStatus: 200, - Interval: 10, - Port: 0, - Type: "http", - Method: "GET", - Timeout: 30, - }) - id, err := service.Create(false) - assert.Nil(t, err) - assert.Equal(t, int64(17), id) -} - -func RunBadServiceCheck(t *testing.T) { - service := core.SelectService(17) - assert.NotNil(t, service) - assert.Equal(t, "Bad Service", service.Name) - for i := 0; i <= 10; i++ { - service.Check(true) - } - assert.True(t, service.IsRunning()) -} - -func RunDeleteService(t *testing.T) { - service := core.SelectService(4) - assert.NotNil(t, service) - assert.Equal(t, "JSON API Tester", service.Name) - assert.False(t, service.IsRunning()) - err := service.Delete() - assert.False(t, service.IsRunning()) - assert.Nil(t, err) -} - -func RunCreateServiceHits(t *testing.T) { - services := core.CoreApp.Services - assert.NotNil(t, services) - assert.Equal(t, 16, len(services)) - for _, service := range services { - service.Check(true) - assert.NotNil(t, service) - } -} - -func RunServiceHits(t *testing.T) { - service := core.SelectService(1) - assert.NotNil(t, service) - hits, err := service.Hits() - assert.Nil(t, err) - assert.NotZero(t, len(hits)) -} - -func RunServiceFailures(t *testing.T) { - service := core.SelectService(17) - assert.NotNil(t, service) - assert.Equal(t, "Bad Service", service.Name) - assert.NotEmpty(t, service.AllFailures()) -} - -func RunServiceLimitedHits(t *testing.T) { - service := core.SelectService(1) - assert.NotNil(t, service) - hits, err := service.LimitedHits() - assert.Nil(t, err) - assert.NotZero(t, len(hits)) -} - -func RunIndexHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/", nil) - assert.Nil(t, err) - rr := httptest.NewRecorder() - route.ServeHTTP(rr, req) - assert.True(t, strings.Contains(rr.Body.String(), "Statup")) - assert.True(t, strings.Contains(rr.Body.String(), "footer")) -} - -func RunServiceHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/service/1", nil) - assert.Nil(t, err) - rr := httptest.NewRecorder() - route.ServeHTTP(rr, req) - assert.True(t, strings.Contains(rr.Body.String(), "Google Status")) - assert.True(t, strings.Contains(rr.Body.String(), "footer")) -} - -func RunPrometheusHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/metrics", nil) - req.Header.Set("Authorization", core.CoreApp.ApiSecret) - assert.Nil(t, err) - rr := httptest.NewRecorder() - route.ServeHTTP(rr, req) - t.Log(rr.Body.String()) - assert.True(t, strings.Contains(rr.Body.String(), "statup_total_services 16")) - assert.True(t, handlers.IsAuthenticated(req)) -} - -func RunFailingPrometheusHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/metrics", nil) - assert.Nil(t, err) - rr := httptest.NewRecorder() - route.ServeHTTP(rr, req) - assert.Equal(t, 303, rr.Result().StatusCode) - assert.True(t, handlers.IsAuthenticated(req)) -} - -func RunLoginHandler(t *testing.T) { - form := url.Values{} - form.Add("username", "admin") - form.Add("password", "password123") - req, err := http.NewRequest("POST", "/dashboard", strings.NewReader(form.Encode())) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - assert.Nil(t, err) - rr := httptest.NewRecorder() - route.ServeHTTP(rr, req) - assert.Equal(t, 200, rr.Result().StatusCode) - assert.True(t, handlers.IsAuthenticated(req)) -} - -func RunDashboardHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/dashboard", nil) - assert.Nil(t, err) - rr := httptest.NewRecorder() - route.ServeHTTP(rr, req) - assert.True(t, strings.Contains(rr.Body.String(), "Statup | Dashboard")) - assert.True(t, strings.Contains(rr.Body.String(), "footer")) - assert.True(t, handlers.IsAuthenticated(req)) -} - -func RunUsersHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/users", nil) - assert.Nil(t, err) - rr := httptest.NewRecorder() - route.ServeHTTP(rr, req) - t.Log(rr.Body.String()) - assert.True(t, strings.Contains(rr.Body.String(), "Statup | Users")) - assert.True(t, strings.Contains(rr.Body.String(), "footer")) - assert.True(t, handlers.IsAuthenticated(req)) -} - -func RunUserViewHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/user/1", nil) - assert.Nil(t, err) - rr := httptest.NewRecorder() - route.ServeHTTP(rr, req) - t.Log(rr.Body.String()) - assert.True(t, strings.Contains(rr.Body.String(), "Statup | testadmin")) - assert.True(t, strings.Contains(rr.Body.String(), "footer")) - assert.True(t, handlers.IsAuthenticated(req)) -} - -func RunServicesHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/services", nil) - assert.Nil(t, err) - rr := httptest.NewRecorder() - route.ServeHTTP(rr, req) - assert.True(t, strings.Contains(rr.Body.String(), "Statup | Services")) - assert.True(t, strings.Contains(rr.Body.String(), "footer")) - assert.True(t, handlers.IsAuthenticated(req)) -} - -func RunHelpHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/help", nil) - assert.Nil(t, err) - rr := httptest.NewRecorder() - route.ServeHTTP(rr, req) - assert.True(t, strings.Contains(rr.Body.String(), "Statup | Help")) - assert.True(t, strings.Contains(rr.Body.String(), "footer")) - assert.True(t, handlers.IsAuthenticated(req)) -} - -func RunSettingsHandler(t *testing.T) { - req, err := http.NewRequest("GET", "/settings", nil) - assert.Nil(t, err) - rr := httptest.NewRecorder() - route.ServeHTTP(rr, req) - assert.True(t, strings.Contains(rr.Body.String(), "Statup | Settings")) - assert.True(t, strings.Contains(rr.Body.String(), "Theme Editor")) - assert.True(t, strings.Contains(rr.Body.String(), "footer")) - assert.True(t, handlers.IsAuthenticated(req)) -} - -func fileExists(file string) bool { - if _, err := os.Stat(file); os.IsNotExist(err) { - return false - } - return true -} diff --git a/core/checker.go b/core/checker.go index e81771ab..ea3bab35 100644 --- a/core/checker.go +++ b/core/checker.go @@ -17,12 +17,10 @@ package core import ( "bytes" - "crypto/tls" "fmt" "github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" - "io/ioutil" "net" "net/http" "net/url" @@ -159,22 +157,14 @@ func (s *Service) checkHttp(record bool) *Service { } s.PingTime = dnsLookup t1 := time.Now() - timeout := time.Duration(time.Duration(s.Timeout) * time.Second) - transport := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - TLSHandshakeTimeout: timeout, - } - client := &http.Client{ - Transport: transport, - Timeout: timeout, - } - var response *http.Response + + timeout := time.Duration(s.Timeout) * time.Second + var content []byte + var res *http.Response if s.Method == "POST" { - response, err = client.Post(s.Domain, "application/json", bytes.NewBuffer([]byte(s.PostData.String))) + content, res, err = utils.HttpRequest(s.Domain, s.Method, "application/json", nil, bytes.NewBuffer([]byte(s.PostData.String)), timeout) } else { - response, err = client.Get(s.Domain) + content, res, err = utils.HttpRequest(s.Domain, s.Method, nil, nil, nil, timeout) } if err != nil { if record { @@ -182,8 +172,6 @@ func (s *Service) checkHttp(record bool) *Service { } return s } - response.Header.Set("Connection", "close") - response.Header.Set("User-Agent", "StatupMonitor") t2 := time.Now() s.Latency = t2.Sub(t1).Seconds() if err != nil { @@ -192,16 +180,14 @@ func (s *Service) checkHttp(record bool) *Service { } return s } - defer response.Body.Close() - contents, err := ioutil.ReadAll(response.Body) - s.LastResponse = string(contents) - s.LastStatusCode = response.StatusCode + s.LastResponse = string(content) + s.LastStatusCode = res.StatusCode if s.Expected.String != "" { if err != nil { utils.Log(2, err) } - match, err := regexp.MatchString(s.Expected.String, string(contents)) + match, err := regexp.MatchString(s.Expected.String, string(content)) if err != nil { utils.Log(2, err) } @@ -212,9 +198,9 @@ func (s *Service) checkHttp(record bool) *Service { return s } } - if s.ExpectedStatus != response.StatusCode { + if s.ExpectedStatus != res.StatusCode { if record { - recordFailure(s, fmt.Sprintf("HTTP Status Code %v did not match %v", response.StatusCode, s.ExpectedStatus)) + recordFailure(s, fmt.Sprintf("HTTP Status Code %v did not match %v", res.StatusCode, s.ExpectedStatus)) } return s } diff --git a/core/database.go b/core/database.go index d8e83c78..d9308144 100644 --- a/core/database.go +++ b/core/database.go @@ -69,7 +69,7 @@ func usersDB() *gorm.DB { // checkinDB returns the Checkin records for a service func checkinDB() *gorm.DB { - return DbSession.Table("checkins").Model(&types.Checkin{}) + return DbSession.Model(&types.Checkin{}) } // checkinHitsDB returns the Checkin Hits records for a service @@ -85,7 +85,7 @@ func messagesDb() *gorm.DB { // HitsBetween returns the gorm database query for a collection of service hits between a time range func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) *gorm.DB { selector := Dbtimestamp(group, column) - if Configs.DbConn == "postgres" { + if CoreApp.DbConnection == "postgres" { timeQuery := fmt.Sprintf("service = %v AND created_at BETWEEN '%v.000000' AND '%v.000000'", s.Id, t1.UTC().Format(types.POSTGRES_TIME), t2.UTC().Format(types.POSTGRES_TIME)) return DbSession.Model(&types.Hit{}).Select(selector).Where(timeQuery) } else { @@ -100,11 +100,6 @@ func CloseDB() { } } -// Close shutsdown the database connection -func (db *DbConfig) Close() error { - return DbSession.DB().Close() -} - // AfterFind for Core will set the timezone func (c *Core) AfterFind() (err error) { c.CreatedAt = utils.Timezoner(c.CreatedAt, CoreApp.Timezone) diff --git a/core/export.go b/core/export.go index ab765043..c888e2b4 100644 --- a/core/export.go +++ b/core/export.go @@ -17,7 +17,9 @@ package core import ( "bytes" + "encoding/json" "github.com/hunterlong/statup/source" + "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "html/template" ) @@ -42,3 +44,28 @@ func ExportChartsJs() string { result := tpl.String() return result } + +type ExportData struct { + Core *types.Core `json:"core"` + Services []types.ServiceInterface `json:"services"` + Messages []*types.Message `json:"messages"` + Checkins []*Checkin `json:"checkins"` + Users []*User `json:"users"` + Notifiers []types.AllNotifiers `json:"notifiers"` +} + +func ExportSettings() ([]byte, error) { + users, err := SelectAllUsers() + if err != nil { + return nil, err + } + data := ExportData{ + Core: CoreApp.Core, + Notifiers: CoreApp.Notifications, + Checkins: AllCheckins(), + Users: users, + Services: CoreApp.Services, + } + export, err := json.Marshal(data) + return export, err +} diff --git a/core/notifier/notifiers.go b/core/notifier/notifiers.go index 1843b419..d19b1c3e 100644 --- a/core/notifier/notifiers.go +++ b/core/notifier/notifiers.go @@ -80,7 +80,9 @@ type NotificationForm struct { DbField string `json:"field"` // true variable key for input SmallText string `json:"small_text"` // insert small text under a html input Required bool `json:"required"` // require this input on the html form - Hidden bool `json:"hidden"` // hide this form element from end user + IsHidden bool `json:"hidden"` // hide this form element from end user + IsList bool `json:"list"` // make this form element a comma separated list + IsSwitch bool `json:"switch"` // make the notifier a boolean true/false switch } // NotificationLog contains the normalized message from previously sent notifications diff --git a/core/sample.go b/core/sample.go index 110ce031..449e203e 100644 --- a/core/sample.go +++ b/core/sample.go @@ -164,7 +164,7 @@ func insertSampleCore() error { } // insertSampleUsers will create 2 admin users for a seed database -func insertSampleUsers() { +func insertSampleUsers() error { u2 := ReturnUser(&types.User{ Username: "testadmin", Password: "password123", @@ -179,11 +179,12 @@ func insertSampleUsers() { Admin: types.NewNullBool(true), }) - u2.Create() - u3.Create() + _, err := u2.Create() + _, err = u3.Create() + return err } -func insertMessages() { +func insertMessages() error { m1 := ReturnMessage(&types.Message{ Title: "Routine Downtime", Description: "This is an example a upcoming message for a service!", @@ -191,8 +192,9 @@ func insertMessages() { StartOn: time.Now().Add(15 * time.Minute), EndOn: time.Now().Add(2 * time.Hour), }) - m1.Create() - + if _, err := m1.Create(); err != nil { + return err + } m2 := ReturnMessage(&types.Message{ Title: "Server Reboot", Description: "This is another example a upcoming message for a service!", @@ -200,16 +202,29 @@ func insertMessages() { StartOn: time.Now().Add(15 * time.Minute), EndOn: time.Now().Add(2 * time.Hour), }) - m2.Create() + if _, err := m2.Create(); err != nil { + return err + } + return nil } // InsertLargeSampleData will create the example/dummy services for testing the Statup server func InsertLargeSampleData() error { - insertSampleCore() - InsertSampleData() - insertSampleUsers() - insertSampleCheckins() - insertMessages() + if err := insertSampleCore(); err != nil { + return err + } + if err := InsertSampleData(); err != nil { + return err + } + if err := insertSampleUsers(); err != nil { + return err + } + if err := insertSampleCheckins(); err != nil { + return err + } + if err := insertMessages(); err != nil { + return err + } s6 := ReturnService(&types.Service{ Name: "JSON Lint", Domain: "https://jsonlint.com", diff --git a/core/services.go b/core/services.go index 6e7bea8e..4a26e01b 100644 --- a/core/services.go +++ b/core/services.go @@ -217,7 +217,7 @@ func (s *Service) DowntimeText() string { // Dbtimestamp will return a SQL query for grouping by date func Dbtimestamp(group string, column string) string { - var seconds int64 + seconds := 3600 switch group { case "minute": seconds = 60 @@ -268,7 +268,10 @@ func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group st return &DateScanObj{[]DateScan{}} } model = model.Order("timeframe asc", false).Group("timeframe") - rows, _ := model.Rows() + rows, err := model.Rows() + if err != nil { + utils.Log(3, fmt.Errorf("issue fetching service chart data: %v", err)) + } for rows.Next() { var gd DateScan diff --git a/handlers/api.go b/handlers/api.go index 3f5b55d9..ea96de7d 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -18,6 +18,7 @@ package handlers import ( "encoding/json" "errors" + "fmt" "github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/types" @@ -62,6 +63,7 @@ func apiRenewHandler(w http.ResponseWriter, r *http.Request) { } func sendErrorJson(err error, w http.ResponseWriter, r *http.Request) { + utils.Log(2, fmt.Errorf("sending error response for %v: %v", r.URL.String(), err.Error())) output := apiResponse{ Status: "error", Error: err.Error(), diff --git a/handlers/dashboard.go b/handlers/dashboard.go index 24ee5339..f5e84568 100644 --- a/handlers/dashboard.go +++ b/handlers/dashboard.go @@ -17,11 +17,9 @@ package handlers import ( "bytes" - "encoding/json" "github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/source" - "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "net/http" "strconv" @@ -99,15 +97,6 @@ func logsLineHandler(w http.ResponseWriter, r *http.Request) { } } -type exportData struct { - Core *types.Core `json:"core"` - Services []types.ServiceInterface `json:"services"` - Messages []*types.Message `json:"messages"` - Checkins []*core.Checkin `json:"checkins"` - Users []*core.User `json:"users"` - Notifiers []types.AllNotifiers `json:"notifiers"` -} - func exportHandler(w http.ResponseWriter, r *http.Request) { if !IsAuthenticated(r) { w.WriteHeader(http.StatusInternalServerError) @@ -120,17 +109,7 @@ func exportHandler(w http.ResponseWriter, r *http.Request) { notifiers = append(notifiers, notifier.Select()) } - users, _ := core.SelectAllUsers() - - data := exportData{ - Core: core.CoreApp.Core, - Notifiers: core.CoreApp.Notifications, - Checkins: core.AllCheckins(), - Users: users, - Services: core.CoreApp.Services, - } - - export, _ := json.Marshal(data) + export, _ := core.ExportSettings() mime := http.DetectContentType(export) fileSize := len(string(export)) diff --git a/notifiers/command.go b/notifiers/command.go index 6207280f..3a11c508 100644 --- a/notifiers/command.go +++ b/notifiers/command.go @@ -16,13 +16,9 @@ package notifiers import ( - "errors" "github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" - "io" - "os" - "os/exec" "time" ) @@ -69,59 +65,10 @@ func init() { } func runCommand(app, cmd string) (string, string, error) { - testCmd := exec.Command(app, "-c", cmd) - - var stdout, stderr []byte - var errStdout, errStderr error - stdoutIn, _ := testCmd.StdoutPipe() - stderrIn, _ := testCmd.StderrPipe() - testCmd.Start() - - go func() { - stdout, errStdout = copyAndCapture(os.Stdout, stdoutIn) - }() - - go func() { - stderr, errStderr = copyAndCapture(os.Stderr, stderrIn) - }() - - err := testCmd.Wait() - if err != nil { - return "", "", err - } - - if errStdout != nil || errStderr != nil { - return "", "", errors.New("failed to capture stdout or stderr") - } - - outStr, errStr := string(stdout), string(stderr) + outStr, errStr, err := utils.Command(cmd) return outStr, errStr, err } -// copyAndCapture captures the response from a terminal command -func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) { - var out []byte - buf := make([]byte, 1024, 1024) - for { - n, err := r.Read(buf[:]) - if n > 0 { - d := buf[:n] - out = append(out, d...) - _, err := w.Write(d) - if err != nil { - return out, err - } - } - if err != nil { - // Read returns io.EOF at the end of file, which is not an error for us - if err == io.EOF { - err = nil - } - return out, err - } - } -} - func (u *commandLine) Select() *notifier.Notification { return u.Notification } diff --git a/notifiers/discord.go b/notifiers/discord.go index 4b55f19c..ccea0dd8 100644 --- a/notifiers/discord.go +++ b/notifiers/discord.go @@ -22,8 +22,8 @@ import ( "fmt" "github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/types" - "io/ioutil" - "net/http" + "github.com/hunterlong/statup/utils" + "strings" "time" ) @@ -59,14 +59,8 @@ func init() { // Send will send a HTTP Post to the discord API. It accepts type: []byte func (u *discord) Send(msg interface{}) error { message := msg.(string) - req, _ := http.NewRequest("POST", discorder.GetValue("host"), bytes.NewBuffer([]byte(message))) - req.Header.Set("Content-Type", "application/json") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return err - } - return resp.Body.Close() + _, _, err := utils.HttpRequest(discorder.GetValue("host"), "POST", "application/json", nil, strings.NewReader(message), time.Duration(10*time.Second)) + return err } func (u *discord) Select() *notifier.Notification { @@ -101,15 +95,7 @@ func (u *discord) OnSave() error { func (u *discord) OnTest() error { outError := errors.New("Incorrect discord URL, please confirm URL is correct") message := `{"content": "Testing the discord notifier"}` - req, _ := http.NewRequest("POST", discorder.Host, bytes.NewBuffer([]byte(message))) - req.Header.Set("Content-Type", "application/json") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - contents, _ := ioutil.ReadAll(resp.Body) + contents, _, err := utils.HttpRequest(discorder.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(message)), time.Duration(10*time.Second)) if string(contents) == "" { return nil } diff --git a/notifiers/email_test.go b/notifiers/email_test.go index 38cc738c..cd33899a 100644 --- a/notifiers/email_test.go +++ b/notifiers/email_test.go @@ -26,12 +26,12 @@ import ( ) var ( - EMAIL_HOST = os.Getenv("EMAIL_HOST") - EMAIL_USER = os.Getenv("EMAIL_USER") - EMAIL_PASS = os.Getenv("EMAIL_PASS") - EMAIL_OUTGOING = os.Getenv("EMAIL_OUTGOING") - EMAIL_SEND_TO = os.Getenv("EMAIL_SEND_TO") - EMAIL_PORT = utils.StringInt(os.Getenv("EMAIL_PORT")) + EMAIL_HOST string + EMAIL_USER string + EMAIL_PASS string + EMAIL_OUTGOING string + EMAIL_SEND_TO string + EMAIL_PORT int64 ) var testEmail *emailOutgoing @@ -42,7 +42,7 @@ func init() { EMAIL_PASS = os.Getenv("EMAIL_PASS") EMAIL_OUTGOING = os.Getenv("EMAIL_OUTGOING") EMAIL_SEND_TO = os.Getenv("EMAIL_SEND_TO") - EMAIL_PORT = utils.StringInt(os.Getenv("EMAIL_PORT")) + EMAIL_PORT = utils.ToInt(os.Getenv("EMAIL_PORT")) emailer.Host = EMAIL_HOST emailer.Username = EMAIL_USER diff --git a/notifiers/line_notify.go b/notifiers/line_notify.go index bd19c896..5f280a9f 100644 --- a/notifiers/line_notify.go +++ b/notifiers/line_notify.go @@ -20,9 +20,9 @@ import ( "github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" - "net/http" "net/url" "strings" + "time" ) const ( @@ -59,21 +59,11 @@ func init() { // Send will send a HTTP Post with the Authorization to the notify-api.line.me server. It accepts type: string func (u *lineNotifier) Send(msg interface{}) error { message := msg.(string) - client := new(http.Client) v := url.Values{} v.Set("message", message) - req, err := http.NewRequest("POST", "https://notify-api.line.me/api/notify", strings.NewReader(v.Encode())) - if err != nil { - return err - } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %v", u.GetValue("api_secret"))) - req.Header.Add("Accept", "application/json") - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - _, err = client.Do(req) - if err != nil { - return err - } - return nil + headers := []string{fmt.Sprintf("Authorization=Bearer %v", u.GetValue("api_secret"))} + _, _, err := utils.HttpRequest("https://notify-api.line.me/api/notify", "POST", "application/x-www-form-urlencoded", headers, strings.NewReader(v.Encode()), time.Duration(10*time.Second)) + return err } func (u *lineNotifier) Select() *notifier.Notification { diff --git a/notifiers/mobile.go b/notifiers/mobile.go index 635c7c8f..cb82a4a0 100644 --- a/notifiers/mobile.go +++ b/notifiers/mobile.go @@ -33,7 +33,7 @@ var mobile = &mobilePush{¬ifier.Notification{ Method: "mobile", Title: "Mobile Notifications", Description: `Receive push notifications on your Android or iPhone devices using the Statup App. You can scan the Authentication QR Code found in Settings to get the mobile app setup in seconds. -

`, +

`, Author: "Hunter Long", AuthorUrl: "https://github.com/hunterlong", Delay: time.Duration(5 * time.Second), @@ -43,7 +43,7 @@ var mobile = &mobilePush{¬ifier.Notification{ Title: "Device Identifiers", Placeholder: "A list of your mobile device push notification ID's.", DbField: "var1", - Hidden: true, + IsHidden: true, }}}, } diff --git a/notifiers/slack.go b/notifiers/slack.go index 65f1ec77..e60b78f1 100644 --- a/notifiers/slack.go +++ b/notifiers/slack.go @@ -21,8 +21,8 @@ import ( "fmt" "github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/types" - "io/ioutil" - "net/http" + "github.com/hunterlong/statup/utils" + "strings" "text/template" "time" ) @@ -85,14 +85,8 @@ func init() { // Send will send a HTTP Post to the slack webhooker API. It accepts type: string func (u *slack) Send(msg interface{}) error { message := msg.(string) - client := new(http.Client) - res, err := client.Post(u.Host, "application/json", bytes.NewBuffer([]byte(message))) - if err != nil { - return err - } - defer res.Body.Close() - //contents, _ := ioutil.ReadAll(res.Body) - return nil + _, _, err := utils.HttpRequest(u.Host, "POST", "application/json", nil, strings.NewReader(message), time.Duration(10*time.Second)) + return err } func (u *slack) Select() *notifier.Notification { @@ -100,13 +94,7 @@ func (u *slack) Select() *notifier.Notification { } func (u *slack) OnTest() error { - client := new(http.Client) - res, err := client.Post(u.Host, "application/json", bytes.NewBuffer([]byte(`{"text":"testing message"}`))) - if err != nil { - return err - } - defer res.Body.Close() - contents, _ := ioutil.ReadAll(res.Body) + contents, _, err := utils.HttpRequest(u.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(`{"text":"testing message"}`)), time.Duration(10*time.Second)) if string(contents) != "ok" { return errors.New("The slack response was incorrect, check the URL") } diff --git a/notifiers/twilio.go b/notifiers/twilio.go index 83ff1542..2e57e04e 100644 --- a/notifiers/twilio.go +++ b/notifiers/twilio.go @@ -22,8 +22,6 @@ import ( "github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" - "io/ioutil" - "net/http" "net/url" "strings" "time" @@ -84,32 +82,21 @@ func (u *twilio) Select() *notifier.Notification { func (u *twilio) Send(msg interface{}) error { message := msg.(string) twilioUrl := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%v/Messages.json", u.GetValue("api_key")) - client := &http.Client{} + v := url.Values{} v.Set("To", "+"+u.Var1) v.Set("From", "+"+u.Var2) v.Set("Body", message) rb := *strings.NewReader(v.Encode()) - req, err := http.NewRequest("POST", twilioUrl, &rb) - if err != nil { - return err - } - req.SetBasicAuth(u.ApiKey, u.ApiSecret) - req.Header.Add("Accept", "application/json") - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - res, err := client.Do(req) - if err != nil { - return err - } - defer res.Body.Close() - contents, _ := ioutil.ReadAll(res.Body) + + contents, _, err := utils.HttpRequest(twilioUrl, "POST", "application/x-www-form-urlencoded", nil, &rb, time.Duration(10*time.Second)) success, _ := twilioSuccess(contents) if !success { errorOut := twilioError(contents) out := fmt.Sprintf("Error code %v - %v", errorOut.Code, errorOut.Message) return errors.New(out) } - return nil + return err } // OnFailure will trigger failing service diff --git a/source/tmpl/form_message.html b/source/tmpl/form_message.html index 1da0eed7..f36bf04a 100644 --- a/source/tmpl/form_message.html +++ b/source/tmpl/form_message.html @@ -59,7 +59,14 @@
- +
+ + +
diff --git a/source/tmpl/form_notifier.html b/source/tmpl/form_notifier.html index f4566d4f..da733e3c 100644 --- a/source/tmpl/form_notifier.html +++ b/source/tmpl/form_notifier.html @@ -6,14 +6,14 @@ {{range $n.Form}}
- + {{if eq .Type "textarea"}} - + {{else}} - + {{end}} {{if .SmallText}} - {{safe .SmallText}} + {{safe .SmallText}} {{end}}
{{end}} diff --git a/source/tmpl/form_service.html b/source/tmpl/form_service.html index 6d2e1708..a0051cb9 100644 --- a/source/tmpl/form_service.html +++ b/source/tmpl/form_service.html @@ -59,7 +59,7 @@
- + A status code of 200 is success, or view all the HTTP Status Codes
@@ -90,6 +90,15 @@ You can also drag and drop services to reorder on the Services tab. +
+ +
+ + + + +
+
diff --git a/source/tmpl/postman.json b/source/tmpl/postman.json index 1803d25d..ac2cebc1 100644 --- a/source/tmpl/postman.json +++ b/source/tmpl/postman.json @@ -940,8 +940,12 @@ "exec": [ "pm.test(\"Create Message\", function () {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.output.title).to.eql(\"API Message\");", - " pm.expect(jsonData.output.service).to.eql(1);", + " var object = jsonData.output;", + " pm.expect(object.title).to.eql(\"API Message\");", + " pm.expect(object.description).to.eql(\"This is an example a upcoming message for a service!\");", + " pm.expect(object.service).to.eql(1);", + " pm.expect(object.notify_before).to.eql(6);", + " pm.expect(object.notify_before_scale).to.eql(\"hour\");", "});" ], "type": "text/javascript" @@ -960,7 +964,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"API Message\",\n \"description\": \"This is an example a upcoming message for a service!\",\n \"start_on\": \"2022-11-17T03:28:16.323797-08:00\",\n \"end_on\": \"2022-11-17T05:13:16.323798-08:00\",\n \"service\": 1,\n \"notify_users\": null,\n \"notify_method\": \"\",\n \"notify_before\": 0\n}" + "raw": "{\n \"title\": \"API Message\",\n \"description\": \"This is an example a upcoming message for a service!\",\n \"start_on\": \"2022-11-17T03:28:16.323797-08:00\",\n \"end_on\": \"2022-11-17T05:13:16.323798-08:00\",\n \"service\": 1,\n \"notify_users\": true,\n \"notify_method\": \"email\",\n \"notify_before\": 6,\n \"notify_before_scale\": \"hour\"\n}" }, "url": { "raw": "{{endpoint}}/api/messages", @@ -981,7 +985,7 @@ { "listen": "test", "script": { - "id": "abbb5178-9613-418c-b5ee-be2d6b4fdb8f", + "id": "c30cc333-53f4-4e9a-9c32-958c905ec163", "exec": [ "pm.test(\"View Message\", function () {", " var jsonData = pm.response.json();", @@ -1020,13 +1024,16 @@ { "listen": "test", "script": { - "id": "a0403c03-0838-4fd2-9cce-aebaf8a128c3", + "id": "e9dd78cc-0f38-4516-bf82-38dd3451b2e7", "exec": [ "pm.test(\"Update Message\", function () {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.status).to.eql(\"success\");", - " pm.expect(jsonData.method).to.eql(\"update\");", - " pm.expect(jsonData.id).to.eql(1);", + " var object = jsonData.output;", + " pm.expect(object.title).to.eql(\"Updated Message\");", + " pm.expect(object.description).to.eql(\"This message was updated\");", + " pm.expect(object.service).to.eql(1);", + " pm.expect(object.notify_before).to.eql(3);", + " pm.expect(object.notify_before_scale).to.eql(\"hour\");", "});" ], "type": "text/javascript" @@ -1045,7 +1052,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Routine Downtime\",\n \"description\": \"This is an example a upcoming message for a service!\",\n \"start_on\": \"2055-11-17T03:28:16.323797-08:00\",\n \"end_on\": \"2055-11-17T05:13:16.323798-08:00\",\n \"service\": 2,\n \"notify_users\": true,\n \"notify_method\": \"email\",\n \"notify_before\": 900\n}" + "raw": "{\n \"title\": \"Updated Message\",\n \"description\": \"This message was updated\",\n \"start_on\": \"2022-11-17T03:28:16.323797-08:00\",\n \"end_on\": \"2022-11-17T05:13:16.323798-08:00\",\n \"service\": 1,\n \"notify_users\": true,\n \"notify_method\": \"email\",\n \"notify_before\": 3,\n \"notify_before_scale\": \"hour\"\n}" }, "url": { "raw": "{{endpoint}}/api/messages/1", diff --git a/types/message.go b/types/message.go index f4be8599..bcf9c79e 100644 --- a/types/message.go +++ b/types/message.go @@ -21,15 +21,16 @@ import ( // Message is for creating Announcements, Alerts and other messages for the end users type Message struct { - Id int64 `gorm:"primary_key;column:id" json:"id"` - Title string `gorm:"column:title" json:"title"` - Description string `gorm:"column:description" json:"description"` - StartOn time.Time `gorm:"column:start_on" json:"start_on"` - EndOn time.Time `gorm:"column:end_on" json:"end_on"` - ServiceId int64 `gorm:"index;column:service" json:"service"` - NotifyUsers NullBool `gorm:"column:notify_users" json:"notify_users"` - NotifyMethod string `gorm:"column:notify_method" json:"notify_method"` - NotifyBefore time.Duration `gorm:"column:notify_before" json:"notify_before"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at" json:"updated_at"` + Id int64 `gorm:"primary_key;column:id" json:"id"` + Title string `gorm:"column:title" json:"title"` + Description string `gorm:"column:description" json:"description"` + StartOn time.Time `gorm:"column:start_on" json:"start_on"` + EndOn time.Time `gorm:"column:end_on" json:"end_on"` + ServiceId int64 `gorm:"index;column:service" json:"service"` + NotifyUsers NullBool `gorm:"column:notify_users" json:"notify_users"` + NotifyMethod string `gorm:"column:notify_method" json:"notify_method"` + NotifyBefore NullInt64 `gorm:"column:notify_before" json:"notify_before"` + NotifyBeforeScale string `gorm:"column:notify_before_scale" json:"notify_before_scale"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at" json:"updated_at"` } diff --git a/utils/utils.go b/utils/utils.go index e947739c..d5ba54ee 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -16,11 +16,13 @@ package utils import ( + "crypto/tls" "errors" "fmt" "github.com/ararog/timeago" "io" "io/ioutil" + "net/http" "os" "os/exec" "regexp" @@ -221,3 +223,39 @@ func SaveFile(filename string, data []byte) error { err := ioutil.WriteFile(filename, data, 0644) return err } + +// HttpRequest is a global function to send a HTTP request +func HttpRequest(url, method string, content interface{}, headers []string, body io.Reader, timeout time.Duration) ([]byte, *http.Response, error) { + var err error + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + DisableKeepAlives: true, + ResponseHeaderTimeout: timeout, + TLSHandshakeTimeout: timeout, + } + client := &http.Client{ + Transport: transport, + Timeout: timeout, + } + r := new(http.Request) + for _, h := range headers { + keyVal := strings.Split(h, "=") + r.Header.Add(keyVal[0], keyVal[1]) + } + if r, err = http.NewRequest(method, url, body); err != nil { + return nil, nil, err + } + r.Header.Set("User-Agent", "Statup") + if content != nil { + r.Header.Set("Content-Type", content.(string)) + } + var resp *http.Response + if resp, err = client.Do(r); err != nil { + return nil, resp, err + } + defer resp.Body.Close() + contents, err := ioutil.ReadAll(resp.Body) + return contents, resp, err +}