Merge pull request #99 from hunterlong/development

Development
pull/105/head^2 v0.79.88
Hunter Long 2018-11-25 02:26:48 -08:00 committed by GitHub
commit a68993dca7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 290 additions and 895 deletions

View File

@ -1,4 +1,4 @@
VERSION=0.79.87 VERSION=0.79.88
BINARY_NAME=statup BINARY_NAME=statup
GOPATH:=$(GOPATH) GOPATH:=$(GOPATH)
GOCMD=go GOCMD=go

View File

@ -27,7 +27,6 @@ import (
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"io/ioutil" "io/ioutil"
"net/http"
"net/http/httptest" "net/http/httptest"
"time" "time"
) )
@ -35,7 +34,9 @@ import (
// catchCLI will run functions based on the commands sent to Statup // catchCLI will run functions based on the commands sent to Statup
func catchCLI(args []string) error { func catchCLI(args []string) error {
dir := utils.Directory dir := utils.Directory
utils.InitLogs() if err := utils.InitLogs(); err != nil {
return err
}
source.Assets() source.Assets()
loadDotEnvs() loadDotEnvs()
@ -50,22 +51,21 @@ func catchCLI(args []string) error {
} }
return errors.New("end") return errors.New("end")
case "assets": case "assets":
err := source.CreateAllAssets(dir) var err error
if err != nil { if err = source.CreateAllAssets(dir); err != nil {
return err return err
} else {
return errors.New("end")
} }
return errors.New("end")
case "sass": case "sass":
err := source.CompileSASS(dir) if err := source.CompileSASS(dir); err != nil {
if err == nil { return err
return errors.New("end")
} }
return err return errors.New("end")
case "update": case "update":
gitCurrent, err := checkGithubUpdates() var err error
if err != nil { var gitCurrent githubResponse
return nil if gitCurrent, err = checkGithubUpdates(); err != nil {
return err
} }
fmt.Printf("Statup Version: v%v\nLatest Version: %v\n", VERSION, gitCurrent.TagName) fmt.Printf("Statup Version: v%v\nLatest Version: %v\n", VERSION, gitCurrent.TagName)
if VERSION != gitCurrent.TagName[1:] { if VERSION != gitCurrent.TagName[1:] {
@ -73,10 +73,7 @@ func catchCLI(args []string) error {
} else { } else {
fmt.Printf("You have the latest version of Statup!\n") fmt.Printf("You have the latest version of Statup!\n")
} }
if err == nil { return errors.New("end")
return errors.New("end")
}
return nil
case "test": case "test":
cmd := args[1] cmd := args[1]
switch cmd { switch cmd {
@ -84,19 +81,17 @@ func catchCLI(args []string) error {
plugin.LoadPlugins() plugin.LoadPlugins()
} }
return errors.New("end") return errors.New("end")
case "export": case "static":
var err error var err error
fmt.Printf("Statup v%v Exporting Static 'index.html' page...\n", VERSION) fmt.Printf("Statup v%v Exporting Static 'index.html' page...\n", VERSION)
utils.InitLogs() utils.InitLogs()
core.Configs, err = core.LoadConfigFile(dir) if core.Configs, err = core.LoadConfigFile(dir); err != nil {
if err != nil {
utils.Log(4, "config.yml file not found") utils.Log(4, "config.yml file not found")
return err return err
} }
indexSource := ExportIndexHTML() indexSource := ExportIndexHTML()
core.CloseDB() //core.CloseDB()
err = utils.SaveFile(dir+"/index.html", indexSource) if err = utils.SaveFile(dir+"/index.html", indexSource); err != nil {
if err != nil {
utils.Log(4, err) utils.Log(4, err)
return err return err
} }
@ -104,10 +99,45 @@ func catchCLI(args []string) error {
case "help": case "help":
HelpEcho() HelpEcho()
return errors.New("end") 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": case "run":
utils.Log(1, "Running 1 time and saving to database...") utils.Log(1, "Running 1 time and saving to database...")
RunOnce() RunOnce()
core.CloseDB() //core.CloseDB()
fmt.Println("Check is complete.") fmt.Println("Check is complete.")
return errors.New("end") return errors.New("end")
case "env": case "env":
@ -175,42 +205,40 @@ func HelpEcho() {
fmt.Println(" statup version - Returns the current version of Statup") 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 run - Check all services 1 time and then quit")
fmt.Println(" statup assets - Dump all assets used locally to be edited.") 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 sass - Compile .scss files into the css directory")
fmt.Println(" statup test plugins - Test all plugins for required information") 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 env - Show all environment variables being used for Statup")
fmt.Println(" statup update - Attempts to update to the latest version") 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 <file> - Imports settings from a previously saved JSON file.")
fmt.Println(" statup help - Shows the user basic information about Statup") fmt.Println(" statup help - Shows the user basic information about Statup")
fmt.Printf("Flags:\n") fmt.Printf("Flags:\n")
fmt.Println(" -ip 127.0.0.1 - Run HTTP server on specific IP address (default: localhost)") 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.Println(" -port 8080 - Run HTTP server on Port (default: 8080)")
fmt.Printf("Environment Variables:\n") 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(" 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_CONN - Automatic Database connection (sqlite, postgres, mysql)")
fmt.Println(" DB_HOST - Database hostname or IP address") fmt.Println(" DB_HOST - Database hostname or IP address")
fmt.Println(" DB_USER - Database username") fmt.Println(" DB_USER - Database username")
fmt.Println(" DB_PASS - Database password") fmt.Println(" DB_PASS - Database password")
fmt.Println(" DB_PORT - Database port (5432, 3306, ...") fmt.Println(" DB_PORT - Database port (5432, 3306, ...)")
fmt.Println(" DB_DATABASE - Database connection's database name") 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(" 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(" NAME - Set a name for the Statup status page")
fmt.Println(" DESCRIPTION - Set a description 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_USER - Username for administrator account (default: admin)")
fmt.Println(" ADMIN_PASS - Password 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(" * You can insert environment variables into a '.env' file in root directory.")
fmt.Println("Give Statup a Star at https://github.com/hunterlong/statup") fmt.Println("Give Statup a Star at https://github.com/hunterlong/statup")
} }
func checkGithubUpdates() (githubResponse, error) { func checkGithubUpdates() (githubResponse, error) {
var gitResp githubResponse var gitResp githubResponse
response, err := http.Get("https://api.github.com/repos/hunterlong/statup/releases/latest") url := "https://api.github.com/repos/hunterlong/statup/releases/latest"
if err != nil { contents, _, err := utils.HttpRequest(url, "GET", nil, nil, nil, time.Duration(10*time.Second))
return githubResponse{}, err
}
defer response.Body.Close()
contents, err := ioutil.ReadAll(response.Body)
if err != nil { if err != nil {
return githubResponse{}, err return githubResponse{}, err
} }

View File

@ -17,6 +17,7 @@ package main
import ( import (
"github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/source"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"github.com/rendon/testcli" "github.com/rendon/testcli"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -35,7 +36,6 @@ func init() {
} }
func TestStartServerCommand(t *testing.T) { func TestStartServerCommand(t *testing.T) {
Clean()
os.Setenv("DB_CONN", "sqlite") os.Setenv("DB_CONN", "sqlite")
cmd := helperCommand(nil, "") cmd := helperCommand(nil, "")
var got = make(chan string) var got = make(chan string)
@ -61,9 +61,9 @@ func TestHelpCommand(t *testing.T) {
} }
func TestExportCommand(t *testing.T) { func TestExportCommand(t *testing.T) {
cmd := helperCommand(nil, "export") cmd := helperCommand(nil, "static")
var got = make(chan string) var got = make(chan string)
commandAndSleep(cmd, time.Duration(4*time.Second), got) commandAndSleep(cmd, time.Duration(10*time.Second), got)
gg, _ := <-got gg, _ := <-got
t.Log(gg) t.Log(gg)
assert.Contains(t, gg, "Exporting Static 'index.html' page...") assert.Contains(t, gg, "Exporting Static 'index.html' page...")
@ -72,10 +72,12 @@ func TestExportCommand(t *testing.T) {
} }
func TestUpdateCommand(t *testing.T) { func TestUpdateCommand(t *testing.T) {
c := testcli.Command("statup", "update") cmd := helperCommand(nil, "version")
c.Run() var got = make(chan string)
assert.True(t, c.StdoutContains("Statup Version: "+VERSION)) commandAndSleep(cmd, time.Duration(10*time.Second), got)
assert.True(t, c.StdoutContains("Latest Version:")) gg, _ := <-got
t.Log(gg)
assert.Contains(t, gg, "Statup")
} }
func TestAssetsCommand(t *testing.T) { func TestAssetsCommand(t *testing.T) {
@ -122,12 +124,12 @@ func TestSassCLI(t *testing.T) {
} }
func TestUpdateCLI(t *testing.T) { func TestUpdateCLI(t *testing.T) {
t.SkipNow()
run := catchCLI([]string{"update"}) run := catchCLI([]string{"update"})
assert.EqualError(t, run, "end") assert.EqualError(t, run, "end")
} }
func TestTestPackageCLI(t *testing.T) { func TestTestPackageCLI(t *testing.T) {
t.SkipNow()
run := catchCLI([]string{"test", "plugins"}) run := catchCLI([]string{"test", "plugins"})
assert.EqualError(t, run, "end") assert.EqualError(t, run, "end")
} }
@ -138,7 +140,6 @@ func TestHelpCLI(t *testing.T) {
} }
func TestRunOnceCLI(t *testing.T) { func TestRunOnceCLI(t *testing.T) {
t.SkipNow()
run := catchCLI([]string{"run"}) run := catchCLI([]string{"run"})
assert.EqualError(t, run, "end") assert.EqualError(t, run, "end")
} }
@ -146,7 +147,6 @@ func TestRunOnceCLI(t *testing.T) {
func TestEnvCLI(t *testing.T) { func TestEnvCLI(t *testing.T) {
run := catchCLI([]string{"env"}) run := catchCLI([]string{"env"})
assert.Error(t, run) assert.Error(t, run)
core.CloseDB()
Clean() Clean()
} }
@ -167,3 +167,22 @@ func runCommand(c *exec.Cmd, out chan<- string) {
bout, _ := c.CombinedOutput() bout, _ := c.CombinedOutput()
out <- string(bout) 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)
}

View File

@ -26,7 +26,6 @@ import (
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"os" "os"
"os/signal"
) )
var ( var (
@ -74,15 +73,6 @@ func main() {
} }
} }
utils.Log(1, fmt.Sprintf("Starting Statup v%v", VERSION)) 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) core.Configs, err = core.LoadConfigFile(utils.Directory)
if err != nil { if err != nil {

View File

@ -1,611 +0,0 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> 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 <http://www.gnu.org/licenses/>.
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(), "<title>Google Status</title>"))
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(), "<title>Statup | Dashboard</title>"))
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(), "<title>Statup | Users</title>"))
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(), "<title>Statup | testadmin</title>"))
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(), "<title>Statup | Services</title>"))
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(), "<title>Statup | Help</title>"))
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(), "<title>Statup | Settings</title>"))
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
}

View File

@ -17,12 +17,10 @@ package core
import ( import (
"bytes" "bytes"
"crypto/tls"
"fmt" "fmt"
"github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/core/notifier"
"github.com/hunterlong/statup/types" "github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -159,22 +157,14 @@ func (s *Service) checkHttp(record bool) *Service {
} }
s.PingTime = dnsLookup s.PingTime = dnsLookup
t1 := time.Now() t1 := time.Now()
timeout := time.Duration(time.Duration(s.Timeout) * time.Second)
transport := &http.Transport{ timeout := time.Duration(s.Timeout) * time.Second
TLSClientConfig: &tls.Config{ var content []byte
InsecureSkipVerify: true, var res *http.Response
},
TLSHandshakeTimeout: timeout,
}
client := &http.Client{
Transport: transport,
Timeout: timeout,
}
var response *http.Response
if s.Method == "POST" { 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 { } else {
response, err = client.Get(s.Domain) content, res, err = utils.HttpRequest(s.Domain, s.Method, nil, nil, nil, timeout)
} }
if err != nil { if err != nil {
if record { if record {
@ -182,8 +172,6 @@ func (s *Service) checkHttp(record bool) *Service {
} }
return s return s
} }
response.Header.Set("Connection", "close")
response.Header.Set("User-Agent", "StatupMonitor")
t2 := time.Now() t2 := time.Now()
s.Latency = t2.Sub(t1).Seconds() s.Latency = t2.Sub(t1).Seconds()
if err != nil { if err != nil {
@ -192,16 +180,14 @@ func (s *Service) checkHttp(record bool) *Service {
} }
return s return s
} }
defer response.Body.Close() s.LastResponse = string(content)
contents, err := ioutil.ReadAll(response.Body) s.LastStatusCode = res.StatusCode
s.LastResponse = string(contents)
s.LastStatusCode = response.StatusCode
if s.Expected.String != "" { if s.Expected.String != "" {
if err != nil { if err != nil {
utils.Log(2, err) 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 { if err != nil {
utils.Log(2, err) utils.Log(2, err)
} }
@ -212,9 +198,9 @@ func (s *Service) checkHttp(record bool) *Service {
return s return s
} }
} }
if s.ExpectedStatus != response.StatusCode { if s.ExpectedStatus != res.StatusCode {
if record { 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 return s
} }

View File

@ -69,7 +69,7 @@ func usersDB() *gorm.DB {
// checkinDB returns the Checkin records for a service // checkinDB returns the Checkin records for a service
func checkinDB() *gorm.DB { 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 // 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 // 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 { func (s *Service) HitsBetween(t1, t2 time.Time, group string, column string) *gorm.DB {
selector := Dbtimestamp(group, column) 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)) 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) return DbSession.Model(&types.Hit{}).Select(selector).Where(timeQuery)
} else { } 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 // AfterFind for Core will set the timezone
func (c *Core) AfterFind() (err error) { func (c *Core) AfterFind() (err error) {
c.CreatedAt = utils.Timezoner(c.CreatedAt, CoreApp.Timezone) c.CreatedAt = utils.Timezoner(c.CreatedAt, CoreApp.Timezone)

View File

@ -17,7 +17,9 @@ package core
import ( import (
"bytes" "bytes"
"encoding/json"
"github.com/hunterlong/statup/source" "github.com/hunterlong/statup/source"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"html/template" "html/template"
) )
@ -42,3 +44,28 @@ func ExportChartsJs() string {
result := tpl.String() result := tpl.String()
return result 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
}

View File

@ -80,7 +80,9 @@ type NotificationForm struct {
DbField string `json:"field"` // true variable key for input DbField string `json:"field"` // true variable key for input
SmallText string `json:"small_text"` // insert small text under a html input SmallText string `json:"small_text"` // insert small text under a html input
Required bool `json:"required"` // require this input on the html form 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 // NotificationLog contains the normalized message from previously sent notifications

View File

@ -164,7 +164,7 @@ func insertSampleCore() error {
} }
// insertSampleUsers will create 2 admin users for a seed database // insertSampleUsers will create 2 admin users for a seed database
func insertSampleUsers() { func insertSampleUsers() error {
u2 := ReturnUser(&types.User{ u2 := ReturnUser(&types.User{
Username: "testadmin", Username: "testadmin",
Password: "password123", Password: "password123",
@ -179,11 +179,12 @@ func insertSampleUsers() {
Admin: types.NewNullBool(true), Admin: types.NewNullBool(true),
}) })
u2.Create() _, err := u2.Create()
u3.Create() _, err = u3.Create()
return err
} }
func insertMessages() { func insertMessages() error {
m1 := ReturnMessage(&types.Message{ m1 := ReturnMessage(&types.Message{
Title: "Routine Downtime", Title: "Routine Downtime",
Description: "This is an example a upcoming message for a service!", Description: "This is an example a upcoming message for a service!",
@ -191,8 +192,9 @@ func insertMessages() {
StartOn: time.Now().Add(15 * time.Minute), StartOn: time.Now().Add(15 * time.Minute),
EndOn: time.Now().Add(2 * time.Hour), EndOn: time.Now().Add(2 * time.Hour),
}) })
m1.Create() if _, err := m1.Create(); err != nil {
return err
}
m2 := ReturnMessage(&types.Message{ m2 := ReturnMessage(&types.Message{
Title: "Server Reboot", Title: "Server Reboot",
Description: "This is another example a upcoming message for a service!", Description: "This is another example a upcoming message for a service!",
@ -200,16 +202,29 @@ func insertMessages() {
StartOn: time.Now().Add(15 * time.Minute), StartOn: time.Now().Add(15 * time.Minute),
EndOn: time.Now().Add(2 * time.Hour), 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 // InsertLargeSampleData will create the example/dummy services for testing the Statup server
func InsertLargeSampleData() error { func InsertLargeSampleData() error {
insertSampleCore() if err := insertSampleCore(); err != nil {
InsertSampleData() return err
insertSampleUsers() }
insertSampleCheckins() if err := InsertSampleData(); err != nil {
insertMessages() 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{ s6 := ReturnService(&types.Service{
Name: "JSON Lint", Name: "JSON Lint",
Domain: "https://jsonlint.com", Domain: "https://jsonlint.com",

View File

@ -217,7 +217,7 @@ func (s *Service) DowntimeText() string {
// Dbtimestamp will return a SQL query for grouping by date // Dbtimestamp will return a SQL query for grouping by date
func Dbtimestamp(group string, column string) string { func Dbtimestamp(group string, column string) string {
var seconds int64 seconds := 3600
switch group { switch group {
case "minute": case "minute":
seconds = 60 seconds = 60
@ -268,7 +268,10 @@ func GraphDataRaw(service types.ServiceInterface, start, end time.Time, group st
return &DateScanObj{[]DateScan{}} return &DateScanObj{[]DateScan{}}
} }
model = model.Order("timeframe asc", false).Group("timeframe") 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() { for rows.Next() {
var gd DateScan var gd DateScan

View File

@ -18,6 +18,7 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/core/notifier"
"github.com/hunterlong/statup/types" "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) { 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{ output := apiResponse{
Status: "error", Status: "error",
Error: err.Error(), Error: err.Error(),

View File

@ -17,11 +17,9 @@ package handlers
import ( import (
"bytes" "bytes"
"encoding/json"
"github.com/hunterlong/statup/core" "github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/core/notifier"
"github.com/hunterlong/statup/source" "github.com/hunterlong/statup/source"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"net/http" "net/http"
"strconv" "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) { func exportHandler(w http.ResponseWriter, r *http.Request) {
if !IsAuthenticated(r) { if !IsAuthenticated(r) {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
@ -120,17 +109,7 @@ func exportHandler(w http.ResponseWriter, r *http.Request) {
notifiers = append(notifiers, notifier.Select()) notifiers = append(notifiers, notifier.Select())
} }
users, _ := core.SelectAllUsers() export, _ := core.ExportSettings()
data := exportData{
Core: core.CoreApp.Core,
Notifiers: core.CoreApp.Notifications,
Checkins: core.AllCheckins(),
Users: users,
Services: core.CoreApp.Services,
}
export, _ := json.Marshal(data)
mime := http.DetectContentType(export) mime := http.DetectContentType(export)
fileSize := len(string(export)) fileSize := len(string(export))

View File

@ -16,13 +16,9 @@
package notifiers package notifiers
import ( import (
"errors"
"github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/core/notifier"
"github.com/hunterlong/statup/types" "github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"io"
"os"
"os/exec"
"time" "time"
) )
@ -69,59 +65,10 @@ func init() {
} }
func runCommand(app, cmd string) (string, string, error) { func runCommand(app, cmd string) (string, string, error) {
testCmd := exec.Command(app, "-c", cmd) outStr, errStr, err := utils.Command(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)
return outStr, errStr, err 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 { func (u *commandLine) Select() *notifier.Notification {
return u.Notification return u.Notification
} }

View File

@ -22,8 +22,8 @@ import (
"fmt" "fmt"
"github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/core/notifier"
"github.com/hunterlong/statup/types" "github.com/hunterlong/statup/types"
"io/ioutil" "github.com/hunterlong/statup/utils"
"net/http" "strings"
"time" "time"
) )
@ -59,14 +59,8 @@ func init() {
// Send will send a HTTP Post to the discord API. It accepts type: []byte // Send will send a HTTP Post to the discord API. It accepts type: []byte
func (u *discord) Send(msg interface{}) error { func (u *discord) Send(msg interface{}) error {
message := msg.(string) message := msg.(string)
req, _ := http.NewRequest("POST", discorder.GetValue("host"), bytes.NewBuffer([]byte(message))) _, _, err := utils.HttpRequest(discorder.GetValue("host"), "POST", "application/json", nil, strings.NewReader(message), time.Duration(10*time.Second))
req.Header.Set("Content-Type", "application/json") return err
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
return resp.Body.Close()
} }
func (u *discord) Select() *notifier.Notification { func (u *discord) Select() *notifier.Notification {
@ -101,15 +95,7 @@ func (u *discord) OnSave() error {
func (u *discord) OnTest() error { func (u *discord) OnTest() error {
outError := errors.New("Incorrect discord URL, please confirm URL is correct") outError := errors.New("Incorrect discord URL, please confirm URL is correct")
message := `{"content": "Testing the discord notifier"}` message := `{"content": "Testing the discord notifier"}`
req, _ := http.NewRequest("POST", discorder.Host, bytes.NewBuffer([]byte(message))) contents, _, err := utils.HttpRequest(discorder.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(message)), time.Duration(10*time.Second))
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)
if string(contents) == "" { if string(contents) == "" {
return nil return nil
} }

View File

@ -26,12 +26,12 @@ import (
) )
var ( var (
EMAIL_HOST = os.Getenv("EMAIL_HOST") EMAIL_HOST string
EMAIL_USER = os.Getenv("EMAIL_USER") EMAIL_USER string
EMAIL_PASS = os.Getenv("EMAIL_PASS") EMAIL_PASS string
EMAIL_OUTGOING = os.Getenv("EMAIL_OUTGOING") EMAIL_OUTGOING string
EMAIL_SEND_TO = os.Getenv("EMAIL_SEND_TO") EMAIL_SEND_TO string
EMAIL_PORT = utils.StringInt(os.Getenv("EMAIL_PORT")) EMAIL_PORT int64
) )
var testEmail *emailOutgoing var testEmail *emailOutgoing
@ -42,7 +42,7 @@ func init() {
EMAIL_PASS = os.Getenv("EMAIL_PASS") EMAIL_PASS = os.Getenv("EMAIL_PASS")
EMAIL_OUTGOING = os.Getenv("EMAIL_OUTGOING") EMAIL_OUTGOING = os.Getenv("EMAIL_OUTGOING")
EMAIL_SEND_TO = os.Getenv("EMAIL_SEND_TO") 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.Host = EMAIL_HOST
emailer.Username = EMAIL_USER emailer.Username = EMAIL_USER

View File

@ -20,9 +20,9 @@ import (
"github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/core/notifier"
"github.com/hunterlong/statup/types" "github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"net/http"
"net/url" "net/url"
"strings" "strings"
"time"
) )
const ( 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 // 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 { func (u *lineNotifier) Send(msg interface{}) error {
message := msg.(string) message := msg.(string)
client := new(http.Client)
v := url.Values{} v := url.Values{}
v.Set("message", message) v.Set("message", message)
req, err := http.NewRequest("POST", "https://notify-api.line.me/api/notify", strings.NewReader(v.Encode())) headers := []string{fmt.Sprintf("Authorization=Bearer %v", u.GetValue("api_secret"))}
if err != nil { _, _, 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 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
} }
func (u *lineNotifier) Select() *notifier.Notification { func (u *lineNotifier) Select() *notifier.Notification {

View File

@ -33,7 +33,7 @@ var mobile = &mobilePush{&notifier.Notification{
Method: "mobile", Method: "mobile",
Title: "Mobile Notifications", 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. 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.
<p align="center"><a href="https://play.google.com/store/apps/details?id=com.statup"><img src="https://img.cjx.io/google-play.svg"></a> <a href="#"><img src="https://img.cjx.io/app-store-badge.svg"></a></p>`, <p align="center"><a href="https://play.google.com/store/apps/details?id=com.statup"><img src="https://img.cjx.io/google-play.svg"></a> <a href="https://testflight.apple.com/join/TuBIj25Q"><img src="https://img.cjx.io/app-store-badge.svg"></a></p>`,
Author: "Hunter Long", Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong", AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(5 * time.Second), Delay: time.Duration(5 * time.Second),
@ -43,7 +43,7 @@ var mobile = &mobilePush{&notifier.Notification{
Title: "Device Identifiers", Title: "Device Identifiers",
Placeholder: "A list of your mobile device push notification ID's.", Placeholder: "A list of your mobile device push notification ID's.",
DbField: "var1", DbField: "var1",
Hidden: true, IsHidden: true,
}}}, }}},
} }

View File

@ -21,8 +21,8 @@ import (
"fmt" "fmt"
"github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/core/notifier"
"github.com/hunterlong/statup/types" "github.com/hunterlong/statup/types"
"io/ioutil" "github.com/hunterlong/statup/utils"
"net/http" "strings"
"text/template" "text/template"
"time" "time"
) )
@ -85,14 +85,8 @@ func init() {
// Send will send a HTTP Post to the slack webhooker API. It accepts type: string // Send will send a HTTP Post to the slack webhooker API. It accepts type: string
func (u *slack) Send(msg interface{}) error { func (u *slack) Send(msg interface{}) error {
message := msg.(string) message := msg.(string)
client := new(http.Client) _, _, err := utils.HttpRequest(u.Host, "POST", "application/json", nil, strings.NewReader(message), time.Duration(10*time.Second))
res, err := client.Post(u.Host, "application/json", bytes.NewBuffer([]byte(message))) return err
if err != nil {
return err
}
defer res.Body.Close()
//contents, _ := ioutil.ReadAll(res.Body)
return nil
} }
func (u *slack) Select() *notifier.Notification { func (u *slack) Select() *notifier.Notification {
@ -100,13 +94,7 @@ func (u *slack) Select() *notifier.Notification {
} }
func (u *slack) OnTest() error { func (u *slack) OnTest() error {
client := new(http.Client) contents, _, err := utils.HttpRequest(u.Host, "POST", "application/json", nil, bytes.NewBuffer([]byte(`{"text":"testing message"}`)), time.Duration(10*time.Second))
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)
if string(contents) != "ok" { if string(contents) != "ok" {
return errors.New("The slack response was incorrect, check the URL") return errors.New("The slack response was incorrect, check the URL")
} }

View File

@ -22,8 +22,6 @@ import (
"github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/core/notifier"
"github.com/hunterlong/statup/types" "github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils" "github.com/hunterlong/statup/utils"
"io/ioutil"
"net/http"
"net/url" "net/url"
"strings" "strings"
"time" "time"
@ -84,32 +82,21 @@ func (u *twilio) Select() *notifier.Notification {
func (u *twilio) Send(msg interface{}) error { func (u *twilio) Send(msg interface{}) error {
message := msg.(string) message := msg.(string)
twilioUrl := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%v/Messages.json", u.GetValue("api_key")) 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 := url.Values{}
v.Set("To", "+"+u.Var1) v.Set("To", "+"+u.Var1)
v.Set("From", "+"+u.Var2) v.Set("From", "+"+u.Var2)
v.Set("Body", message) v.Set("Body", message)
rb := *strings.NewReader(v.Encode()) rb := *strings.NewReader(v.Encode())
req, err := http.NewRequest("POST", twilioUrl, &rb)
if err != nil { contents, _, err := utils.HttpRequest(twilioUrl, "POST", "application/x-www-form-urlencoded", nil, &rb, time.Duration(10*time.Second))
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)
success, _ := twilioSuccess(contents) success, _ := twilioSuccess(contents)
if !success { if !success {
errorOut := twilioError(contents) errorOut := twilioError(contents)
out := fmt.Sprintf("Error code %v - %v", errorOut.Code, errorOut.Message) out := fmt.Sprintf("Error code %v - %v", errorOut.Code, errorOut.Message)
return errors.New(out) return errors.New(out)
} }
return nil return err
} }
// OnFailure will trigger failing service // OnFailure will trigger failing service

View File

@ -59,7 +59,14 @@
<div class="form-group row"> <div class="form-group row">
<label for="notify_before" class="col-sm-4 col-form-label">Notify Before</label> <label for="notify_before" class="col-sm-4 col-form-label">Notify Before</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" name="notify_before" class="form-control" id="notify_before" value="{{.NotifyBefore}}"> <div class="form-inline">
<input type="number" name="notify_before_scale" class="col-4 form-control" id="notify_before" value="{{.NotifyBefore.Int64}}" >
<select class="ml-2 col-7 form-control" name="notify_before_scale" id="notify_before_scale">
<option value="minute"{{if ne .Id 0}} selected{{end}}>Minutes</option>
<option value="hour">Hours</option>
<option value="day">Days</option>
</select>
</div>
</div> </div>
</div> </div>

View File

@ -6,14 +6,14 @@
{{range $n.Form}} {{range $n.Form}}
<div class="form-group"> <div class="form-group">
<label class="text-capitalize{{if .Hidden}} d-none{{end}}" for="{{underscore .Title}}">{{.Title}}</label> <label class="text-capitalize{{if .IsHidden}} d-none{{end}}" for="{{underscore .Title}}">{{.Title}}</label>
{{if eq .Type "textarea"}} {{if eq .Type "textarea"}}
<textarea rows="3" class="form-control{{if .Hidden}} d-none{{end}}" name="{{underscore .DbField}}" id="{{underscore .Title}}">{{ $n.GetValue .DbField }}</textarea> <textarea rows="3" class="form-control{{if .IsHidden}} d-none{{end}}" name="{{underscore .DbField}}" id="{{underscore .Title}}">{{ $n.GetValue .DbField }}</textarea>
{{else}} {{else}}
<input type="{{.Type}}" name="{{underscore .DbField}}" class="form-control{{if .Hidden}} d-none{{end}}" value="{{ $n.GetValue .DbField }}" id="{{underscore .Title}}" placeholder="{{.Placeholder}}" {{if .Required}}required{{end}}> <input type="{{.Type}}" name="{{underscore .DbField}}" class="form-control{{if .IsHidden}} d-none{{end}}" value="{{ $n.GetValue .DbField }}" id="{{underscore .Title}}" placeholder="{{.Placeholder}}" {{if .Required}}required{{end}}>
{{end}} {{end}}
{{if .SmallText}} {{if .SmallText}}
<small class="form-text text-muted{{if .Hidden}} d-none{{end}}">{{safe .SmallText}}</small> <small class="form-text text-muted{{if .IsHidden}} d-none{{end}}">{{safe .SmallText}}</small>
{{end}} {{end}}
</div> </div>
{{end}} {{end}}

View File

@ -59,7 +59,7 @@
<div class="form-group row{{if (eq .Type "tcp") or (eq .Type "udp")}} d-none{{end}}"> <div class="form-group row{{if (eq .Type "tcp") or (eq .Type "udp")}} d-none{{end}}">
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label> <label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" value="{{if ne .ExpectedStatus 0}}{{.ExpectedStatus}}{{end}}" placeholder="200" id="service_response_code"> <input type="number" name="expected_status" class="form-control" value="{{if ne .ExpectedStatus 0}}{{.ExpectedStatus}}{{else}}200{{end}}" placeholder="200" id="service_response_code">
<small class="form-text text-muted">A status code of 200 is success, or view all the <a target="_blank" href="https://www.restapitutorial.com/httpstatuscodes.html">HTTP Status Codes</a></small> <small class="form-text text-muted">A status code of 200 is success, or view all the <a target="_blank" href="https://www.restapitutorial.com/httpstatuscodes.html">HTTP Status Codes</a></small>
</div> </div>
</div> </div>
@ -90,6 +90,15 @@
<small class="form-text text-muted">You can also drag and drop services to reorder on the Services tab.</small> <small class="form-text text-muted">You can also drag and drop services to reorder on the Services tab.</small>
</div> </div>
</div> </div>
<div class="form-group row">
<label for="order" class="col-sm-4 col-form-label">Notifications</label>
<div class="col-8 mt-1">
<span class="switch float-left">
<input type="checkbox" name="allow_notifications" class="switch" id="switch-service" {{if eq .Id 0}}checked{{end}}{{if .AllowNotifications.Bool}}checked{{end}}>
<label for="switch-service">Allow notifications to be sent for this service</label>
</span>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<div class="{{if ne .Id 0}}col-6{{else}}col-12{{end}}"> <div class="{{if ne .Id 0}}col-6{{else}}col-12{{end}}">
<button type="submit" class="btn btn-success btn-block">{{if ne .Id 0}}Update Service{{else}}Create Service{{end}}</button> <button type="submit" class="btn btn-success btn-block">{{if ne .Id 0}}Update Service{{else}}Create Service{{end}}</button>

View File

@ -940,8 +940,12 @@
"exec": [ "exec": [
"pm.test(\"Create Message\", function () {", "pm.test(\"Create Message\", function () {",
" var jsonData = pm.response.json();", " var jsonData = pm.response.json();",
" pm.expect(jsonData.output.title).to.eql(\"API Message\");", " var object = jsonData.output;",
" pm.expect(jsonData.output.service).to.eql(1);", " 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" "type": "text/javascript"
@ -960,7 +964,7 @@
], ],
"body": { "body": {
"mode": "raw", "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": { "url": {
"raw": "{{endpoint}}/api/messages", "raw": "{{endpoint}}/api/messages",
@ -981,7 +985,7 @@
{ {
"listen": "test", "listen": "test",
"script": { "script": {
"id": "abbb5178-9613-418c-b5ee-be2d6b4fdb8f", "id": "c30cc333-53f4-4e9a-9c32-958c905ec163",
"exec": [ "exec": [
"pm.test(\"View Message\", function () {", "pm.test(\"View Message\", function () {",
" var jsonData = pm.response.json();", " var jsonData = pm.response.json();",
@ -1020,13 +1024,16 @@
{ {
"listen": "test", "listen": "test",
"script": { "script": {
"id": "a0403c03-0838-4fd2-9cce-aebaf8a128c3", "id": "e9dd78cc-0f38-4516-bf82-38dd3451b2e7",
"exec": [ "exec": [
"pm.test(\"Update Message\", function () {", "pm.test(\"Update Message\", function () {",
" var jsonData = pm.response.json();", " var jsonData = pm.response.json();",
" pm.expect(jsonData.status).to.eql(\"success\");", " var object = jsonData.output;",
" pm.expect(jsonData.method).to.eql(\"update\");", " pm.expect(object.title).to.eql(\"Updated Message\");",
" pm.expect(jsonData.id).to.eql(1);", " 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" "type": "text/javascript"
@ -1045,7 +1052,7 @@
], ],
"body": { "body": {
"mode": "raw", "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": { "url": {
"raw": "{{endpoint}}/api/messages/1", "raw": "{{endpoint}}/api/messages/1",

View File

@ -21,15 +21,16 @@ import (
// Message is for creating Announcements, Alerts and other messages for the end users // Message is for creating Announcements, Alerts and other messages for the end users
type Message struct { type Message struct {
Id int64 `gorm:"primary_key;column:id" json:"id"` Id int64 `gorm:"primary_key;column:id" json:"id"`
Title string `gorm:"column:title" json:"title"` Title string `gorm:"column:title" json:"title"`
Description string `gorm:"column:description" json:"description"` Description string `gorm:"column:description" json:"description"`
StartOn time.Time `gorm:"column:start_on" json:"start_on"` StartOn time.Time `gorm:"column:start_on" json:"start_on"`
EndOn time.Time `gorm:"column:end_on" json:"end_on"` EndOn time.Time `gorm:"column:end_on" json:"end_on"`
ServiceId int64 `gorm:"index;column:service" json:"service"` ServiceId int64 `gorm:"index;column:service" json:"service"`
NotifyUsers NullBool `gorm:"column:notify_users" json:"notify_users"` NotifyUsers NullBool `gorm:"column:notify_users" json:"notify_users"`
NotifyMethod string `gorm:"column:notify_method" json:"notify_method"` NotifyMethod string `gorm:"column:notify_method" json:"notify_method"`
NotifyBefore time.Duration `gorm:"column:notify_before" json:"notify_before"` NotifyBefore NullInt64 `gorm:"column:notify_before" json:"notify_before"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at" json:"created_at"` NotifyBeforeScale string `gorm:"column:notify_before_scale" json:"notify_before_scale"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at" json:"updated_at"` 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"`
} }

View File

@ -16,11 +16,13 @@
package utils package utils
import ( import (
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"github.com/ararog/timeago" "github.com/ararog/timeago"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http"
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
@ -221,3 +223,39 @@ func SaveFile(filename string, data []byte) error {
err := ioutil.WriteFile(filename, data, 0644) err := ioutil.WriteFile(filename, data, 0644)
return err 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
}