From 7c9a41ef20308abb158f740f71f4b1e83153b46d Mon Sep 17 00:00:00 2001 From: Savas Ziplies Date: Tue, 21 Jul 2020 09:33:56 +0200 Subject: [PATCH] Changed: File structure Added: Average ms calculation for multiple loops Added: Bootstrap HTML template with sortable Table Added: JSON output --- README.md | 13 ++- build.sh | 2 +- command.go | 170 +++++++++++++++++++++++++++ main.go | 329 ++++++++--------------------------------------------- model.go | 60 ++++++++++ output.go | 153 +++++++++++++++++++++++++ util.go | 60 ++++++++++ 7 files changed, 504 insertions(+), 283 deletions(-) create mode 100644 command.go create mode 100644 model.go create mode 100644 output.go create mode 100644 util.go diff --git a/README.md b/README.md index 08cac5c..4667463 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,13 @@ A simple API Ping tool to feed an OpenAPI 3.0 description file and call all endp * Pass custom headers, e.g. `Authorization` * Create random `integer` and `string` parameters for urls * Track the time and response body per request -* Output the results to console, CSV, HTML or Markdown +* Output the results to console, CSV, HTML, JSON or Markdown ## Latest Versions +* 0.3.0 + * Added average ms calculation for multiple loops + * Added Bootstrap HTML template with sortable Table + * Added JSON output * 0.2.0 * Added an option to configure included query methods * 0.1.0 @@ -35,7 +39,7 @@ Usage -loop int How often to loop through all calls (default 1) -out string - The output format. Options: console, csv, html, md (default "console") + The output format. Options: console, csv, html, md, json (default "console") -response Include the response body in the output -timeout int @@ -75,6 +79,9 @@ How many parallel processes should be spawned to query your endpoints. #### Output Define an output format. The output is written to a local `aping.XYZ` file, depending on your choice. +#### Loop +*If `loop > 1` is mixed with `response` all responses are logged!* + ## Build [Download and install][5] Golang for your platform. @@ -93,7 +100,7 @@ aPing is not fully-fledged (yet). Some functionality is missing and errors may o Known issues are: * Endpoints having request bodies are not pinged -* Parameters aside `integer` and `string` are not pinged +* Parameters besides `integer` and `string` are not pinged ## License and Credits aPing is released under the MIT license by [elipZis][1]. diff --git a/build.sh b/build.sh index c590b33..c7f4cdf 100644 --- a/build.sh +++ b/build.sh @@ -8,7 +8,7 @@ fi package_split=(${package//\// }) package_name=${package_split[-1]} -version=0.2.0 +version=0.3.0 platforms=("windows/amd64" "windows/386" "linux/amd64" "linux/386") for platform in "${platforms[@]}" diff --git a/command.go b/command.go new file mode 100644 index 0000000..7bde81e --- /dev/null +++ b/command.go @@ -0,0 +1,170 @@ +package main + +import ( + "bufio" + "encoding/json" + "flag" + "fmt" + "github.com/getkin/kin-openapi/openapi3" + "github.com/jedib0t/go-pretty/progress" + "log" + "os" + "strconv" + "strings" + "time" +) + +// Methods to exclude +var QueryMethods []string + +// Define the possible command line arguments +var ( + inputFlag = flag.String("input", "", "*The path/url to the Swagger/OpenAPI 3.0 input source") + basePathFlag = flag.String("base", "", "The base url to query") + outputFlag = flag.String("out", "console", "The output format. Options: console, csv, html, md, json") + headerFlag = flag.String("header", "{}", "Pass a custom header as JSON string, e.g. '{\"Authorization\": \"Bearer TOKEN\"}'") + workerFlag = flag.Int("worker", 1, "The amount of parallel workers to use") + timeoutFlag = flag.Int("timeout", 5, "The timeout in seconds per request") + loopFlag = flag.Int("loop", 1, "How often to loop through all calls") + responseFlag = flag.Bool("response", false, "Include the response body in the output") + methodsFlag = flag.String("methods", "[\"GET\",\"POST\"]", "An array of query methods to include, e.g. '[\"GET\", \"POST\"]'") + + basePath string +) + +// Logging output +var progressWriter = progress.NewWriter() + +// Init some short variable options +func init() { + flag.StringVar(inputFlag, "i", "", "*The path/url to the Swagger/OpenAPI 3.0 input source") + flag.StringVar(basePathFlag, "b", "", "The base url to query") + flag.StringVar(outputFlag, "o", "console", "The output format. Options: console, csv, html, md, json") + flag.IntVar(workerFlag, "w", 1, "The amount of parallel workers to use") + flag.IntVar(timeoutFlag, "t", 5, "The timeout in seconds per request") + flag.IntVar(loopFlag, "l", 1, "How often to loop through all calls") + flag.BoolVar(responseFlag, "r", false, "Include the response body in the output") + flag.StringVar(methodsFlag, "m", "[\"GET\",\"POST\"]", "An array of query methods to include, e.g. '[\"GET\", \"POST\"]'") + + // Pre-set the progress writer + progressWriter.SetAutoStop(true) + progressWriter.ShowTime(true) + progressWriter.ShowTracker(true) + progressWriter.ShowValue(true) + progressWriter.SetSortBy(progress.SortByPercentDsc) + progressWriter.SetStyle(progress.StyleBlocks) + progressWriter.SetTrackerPosition(progress.PositionRight) + progressWriter.SetUpdateFrequency(time.Millisecond * 100) + progressWriter.Style().Colors = progress.StyleColorsExample + progressWriter.Style().Options.PercentFormat = "%4.1f%%" +} + +// Parse any given base url or check for Swagger servers +func parseBase(swagger *openapi3.Swagger) { + if basePathFlag == nil || *basePathFlag == "" { + // Check for servers + var servers []string + if swagger.Servers != nil { + for _, v := range swagger.Servers { + serverUrl := v.URL + if v.Variables != nil { + for key, variable := range v.Variables { + serverUrl = strings.Replace(serverUrl, "{"+key+"}", variable.Default.(string), 1) + } + } + servers = append(servers, serverUrl) + } + } + + // + if servers != nil && len(servers) > 0 { + fmt.Println("No base given. Select a server.") + for k, v := range servers { + fmt.Println(fmt.Sprintf("[%d] %s", k, v)) + } + fmt.Print("Pick a server no.: ") + + reader := bufio.NewReader(os.Stdin) + char, _, err := reader.ReadRune() + if err != nil { + log.Fatal(err) + } + + index, err := strconv.Atoi(string(char)) + if err != nil { + log.Fatal(err) + } + if index >= len(servers) { + log.Println("Cannot parse the given input. Please pick one of the given options as simple number!") + parseBase(swagger) + } else { + basePath = servers[index] + } + } + } else { + basePath = *basePathFlag + } +} + +// Parse any given header and override/add it to the global header +func parseHeader() { + var result map[string]string + err := json.Unmarshal([]byte(*headerFlag), &result) + checkFatalError(err) + + for key, value := range result { + Headers[key] = value + } +} + +// Parse all query methods to includefor calls +func parseQueryMethods() { + err := json.Unmarshal([]byte(*methodsFlag), &QueryMethods) + checkFatalError(err) +} + +// Create a "pingable" url with parameters +func parseUrl(path string, operation *openapi3.Operation) (string, bool) { + parsed := true + for _, v := range operation.Parameters { + // Required or path parameter, which is always required + if v.Value.Required || strings.ToLower(v.Value.In) == "path" { + if v.Value.Schema != nil { + var randomParameter string + + // Check supported parameter types + switch v.Value.Schema.Value.Type { + case "integer": + min := 0 + max := 100 + if v.Value.Schema.Value.Min != nil { + min = int(*v.Value.Schema.Value.Min) + } + if v.Value.Schema.Value.Max != nil { + max = int(*v.Value.Schema.Value.Max) + } + randomParameter = strconv.Itoa(seededRand.Intn(max-min+1) + min) + case "string": + length := 1 + if v.Value.Schema.Value.MinLength > 1 { + length = int(v.Value.Schema.Value.MinLength) + } + if v.Value.Schema.Value.MaxLength != nil { + length = int(*v.Value.Schema.Value.MaxLength) + } + randomParameter = getRandString(length) + default: + // Cannot parse at least one parameter => don't ping! + parsed = false + } + // + if randomParameter != "" { + path = strings.Replace(path, "{"+v.Value.Name+"}", randomParameter, 1) + } + } else { + parsed = false + } + } + } + return basePath + path, parsed +} diff --git a/main.go b/main.go index 58094c4..475d746 100644 --- a/main.go +++ b/main.go @@ -14,104 +14,25 @@ package main import ( - "bufio" "encoding/json" "flag" "fmt" "github.com/getkin/kin-openapi/openapi3" "github.com/jedib0t/go-pretty/progress" - "github.com/jedib0t/go-pretty/table" "io/ioutil" "log" - "math/rand" "net/http" - "net/url" - "os" "regexp" - "strconv" "strings" "sync" "time" ) -// A single entry to "ping" -type Ping struct { - Method string `json:"method"` - Url string `json:"url"` - Headers map[string]string `json:"headers"` -} - -// Pre-parse the input to see if it is an openapi 3.0 or swagger 2.0 file -type SwaggerOpenApi struct { - Swagger string `json:"swagger;omitempty"` - OpenAPI string `json:"openapi;omitempty"` -} - -// The default request headers -var Headers = map[string]string{ - "Accept": "*/*", - "Connection": "Keep-Alive", - "Content-Type": "application/json", - "User-Agent": "aPing", -} - -// Methods to exclude -var QueryMethods []string - // The HTTP Client to reuse var client *http.Client -// A pool of Ping objects to reduce the GC overhead -var pingPool = sync.Pool{ - New: func() interface{} { - return new(Ping) - }, -} - -// Define the possible command line arguments -var ( - inputFlag = flag.String("input", "", "*The path/url to the Swagger/OpenAPI 3.0 input source") - basePathFlag = flag.String("base", "", "The base url to query") - outputFlag = flag.String("out", "console", "The output format. Options: console, csv, html, md") - headerFlag = flag.String("header", "{}", "Pass a custom header as JSON string, e.g. '{\"Authorization\": \"Bearer TOKEN\"}'") - workerFlag = flag.Int("worker", 1, "The amount of parallel workers to use") - timeoutFlag = flag.Int("timeout", 5, "The timeout in seconds per request") - loopFlag = flag.Int("loop", 1, "How often to loop through all calls") - responseFlag = flag.Bool("response", false, "Include the response body in the output") - methodsFlag = flag.String("methods", "[\"GET\",\"POST\"]", "An array of query methods to include, e.g. '[\"GET\", \"POST\"]'") - - basePath string -) - -// Logging output -var ( - progressWriter progress.Writer - tableWriter table.Writer - tableColumnConfig = []table.ColumnConfig{ - {Name: "URL"}, - {Name: "Method", WidthMax: 8}, - {Name: "Elapsed ms"}, - {Name: "Response", WidthMax: 100}, - } -) - -// Seed -var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) - -// String charset to randomly pick from -const RandomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - -// Init some short variable options -func init() { - flag.StringVar(inputFlag, "i", "", "*The path/url to the Swagger/OpenAPI 3.0 input source") - flag.StringVar(basePathFlag, "b", "", "The base url to query") - flag.StringVar(outputFlag, "o", "console", "The output format. Options: console, csv, html, md") - flag.IntVar(workerFlag, "w", 1, "The amount of parallel workers to use") - flag.IntVar(timeoutFlag, "t", 5, "The timeout in seconds per request") - flag.IntVar(loopFlag, "l", 1, "How often to loop through all calls") - flag.BoolVar(responseFlag, "r", false, "Include the response body in the output") - flag.StringVar(methodsFlag, "m", "[\"GET\",\"POST\"]", "An array of query methods to include, e.g. '[\"GET\", \"POST\"]'") -} +// Matching pattern for {parameters} in paths +var regExParameterPattern, _ = regexp.Compile("\\{.+\\}") // func main() { @@ -161,11 +82,13 @@ func main() { parseQueryMethods() // + var title string if swagger.Info != nil { - log.Println(fmt.Sprintf("Pinging '%s - %s'", swagger.Info.Title, swagger.Info.Description)) + title = fmt.Sprintf("Pinging '%s - %s'", swagger.Info.Title, swagger.Info.Description) } else { - log.Println(fmt.Sprintf("Pinging '%s - %s'", *inputFlag, *basePathFlag)) + title = fmt.Sprintf("Pinging '%s - %s'", *inputFlag, *basePathFlag) } + log.Println(title) // Create a client with timeout and redirect handler client = &http.Client{ @@ -198,27 +121,10 @@ func main() { } } - // Create a table writer to log to - tableWriter = table.NewWriter() - tableWriter.SetAutoIndex(true) - tableWriter.AppendHeader(table.Row{"URL", "Method", "Elapsed ms", "Response"}) - tableWriter.SetColumnConfigs(tableColumnConfig) - - // Instantiate a Progress Writer and set up the options - progressWriter = progress.NewWriter() - progressWriter.SetAutoStop(true) - progressWriter.ShowTime(true) - progressWriter.ShowTracker(true) - progressWriter.ShowValue(true) + // Set up the Progress Writer options progressWriter.SetNumTrackersExpected(*loopFlag) progressWriter.ShowOverallTracker(*loopFlag > 1) progressWriter.SetTrackerLength(pings) - progressWriter.SetSortBy(progress.SortByPercentDsc) - progressWriter.SetStyle(progress.StyleBlocks) - progressWriter.SetTrackerPosition(progress.PositionRight) - progressWriter.SetUpdateFrequency(time.Millisecond * 100) - progressWriter.Style().Colors = progress.StyleColorsExample - progressWriter.Style().Options.PercentFormat = "%4.1f%%" go progressWriter.Render() // Prepare the progress trackers @@ -231,27 +137,14 @@ func main() { for i := 0; i < *loopFlag; i++ { loop(pings, swagger, &progressTrackers[i]) } + // Wait for the progress writer to finish rendering + for progressWriter.IsRenderInProgress() { + time.Sleep(time.Millisecond * 100) + } progressWriter.Stop() - // If an output file is given, write to it - if outputFlag != nil && *outputFlag != "" { - switch strings.ToLower(*outputFlag) { - case "console": - log.Println("\n" + tableWriter.Render()) - case "csv": - err := ioutil.WriteFile("aping.csv", []byte(tableWriter.RenderCSV()), 0644) - checkError(err) - case "html": - err := ioutil.WriteFile("aping.html", []byte(tableWriter.RenderHTML()), 0644) - checkError(err) - case "md": - err := ioutil.WriteFile("aping.md", []byte(tableWriter.RenderMarkdown()), 0644) - checkError(err) - } - } else { - // Otherwise just print the output - log.Println("\n" + tableWriter.Render()) - } + // Flush the results + flush(title, outputFlag) return } @@ -259,7 +152,7 @@ func main() { flag.Usage() } -// +// Loop once through all paths func loop(pings int, swagger *openapi3.Swagger, progressTracker *progress.Tracker) { // Prepare the channels var waitGroup sync.WaitGroup @@ -287,6 +180,7 @@ func loop(pings int, swagger *openapi3.Swagger, progressTracker *progress.Tracke // Get a pool ping to reuse ping = pingPool.Get().(*Ping) ping.Method = method + ping.Path = path ping.Url = pathUrl ping.Headers = Headers // Fire @@ -301,11 +195,17 @@ func loop(pings int, swagger *openapi3.Swagger, progressTracker *progress.Tracke // Ping the given url with all required headers and information func ping(pings <-chan *Ping, waitGroup *sync.WaitGroup, progressTracker *progress.Tracker) { + var pong *Pong for ping := range pings { + // The response pool reset object + pong = pongPool.Get().(*Pong) + pong.Ping = *ping + pong.Response = "-" + methodName := strings.ToUpper(ping.Method) req, err := http.NewRequest(methodName, ping.Url, nil) if err != nil { - log.Println(err) + pong.Response = fmt.Sprintf("[aPing] The new HTTP request build failed with error: %s", err) } req.Close = true @@ -314,183 +214,54 @@ func ping(pings <-chan *Ping, waitGroup *sync.WaitGroup, progressTracker *progre req.Header.Set(key, value) } - // Fire + // Fire & calculate elapsed ms start := time.Now().UnixNano() response, err := client.Do(req) elapsed := getElapsedTimeInMS(start) + pong.Time = elapsed + // Any error? if err != nil { - tableWriter.AppendRow(table.Row{ping.Url, methodName, elapsed, fmt.Sprintf("[aPing] The HTTP request failed with error %s", err)}) + pong.Response = fmt.Sprintf("[aPing] The HTTP request failed with error: %s", err) } else { if *responseFlag { data, _ := ioutil.ReadAll(response.Body) + // Trim all line breaks from the response for better output re := regexp.MustCompile(`\r?\n`) bodyData := re.ReplaceAllString(string(data), " ") - tableWriter.AppendRow(table.Row{ping.Url, methodName, elapsed, bodyData}) + // Store response + pong.Response = bodyData _ = response.Body.Close() - } else { - tableWriter.AppendRow(table.Row{ping.Url, methodName, elapsed, "-"}) - } - } - - // Clear up - waitGroup.Done() - progressTracker.Increment(1) - } -} - -// Parse any given base url or check for Swagger servers -func parseBase(swagger *openapi3.Swagger) { - if basePathFlag == nil || *basePathFlag == "" { - // Check for servers - var servers []string - if swagger.Servers != nil { - for _, v := range swagger.Servers { - serverUrl := v.URL - if v.Variables != nil { - for key, variable := range v.Variables { - serverUrl = strings.Replace(serverUrl, "{"+key+"}", variable.Default.(string), 1) - } - } - servers = append(servers, serverUrl) } } - // - if servers != nil && len(servers) > 0 { - fmt.Println("No base given. Select a server.") - for k, v := range servers { - fmt.Println(fmt.Sprintf("[%d] %s", k, v)) - } - fmt.Print("Pick a server no.: ") - - reader := bufio.NewReader(os.Stdin) - char, _, err := reader.ReadRune() - if err != nil { - log.Fatal(err) - } - - index, err := strconv.Atoi(string(char)) - if err != nil { - log.Fatal(err) - } - if index >= len(servers) { - log.Println("Cannot parse the given input. Please pick one of the given options as simple number!") - parseBase(swagger) - } else { - basePath = servers[index] - } - } - } else { - basePath = *basePathFlag - } -} - -// Parse any given header and override/add it to the global header -func parseHeader() { - var result map[string]string - err := json.Unmarshal([]byte(*headerFlag), &result) - checkError(err) + // Collect the pongs + collectPong(pong) - for key, value := range result { - Headers[key] = value + // Clear & Count up + progressTracker.Increment(1) + waitGroup.Done() + // Return to the source Neo + pingPool.Put(ping) } } -// Parse all query methods to includefor calls -func parseQueryMethods() { - err := json.Unmarshal([]byte(*methodsFlag), &QueryMethods) - checkError(err) -} - -// Create a "pingable" url with parameters -func parseUrl(path string, operation *openapi3.Operation) (string, bool) { - parsed := true - for _, v := range operation.Parameters { - // Required or path parameter, which is always required - if v.Value.Required || strings.ToLower(v.Value.In) == "path" { - if v.Value.Schema != nil { - var randomParameter string - - // Check supported parameter types - switch v.Value.Schema.Value.Type { - case "integer": - min := 0 - max := 100 - if v.Value.Schema.Value.Min != nil { - min = int(*v.Value.Schema.Value.Min) - } - if v.Value.Schema.Value.Max != nil { - max = int(*v.Value.Schema.Value.Max) - } - randomParameter = strconv.Itoa(seededRand.Intn(max-min+1) + min) - case "string": - length := 1 - if v.Value.Schema.Value.MinLength > 1 { - length = int(v.Value.Schema.Value.MinLength) - } - if v.Value.Schema.Value.MaxLength != nil { - length = int(*v.Value.Schema.Value.MaxLength) - } - randomParameter = getRandString(length) - default: - // Cannot parse at least one parameter => don't ping! - parsed = false - } - // - if randomParameter != "" { - path = strings.Replace(path, "{"+v.Value.Name+"}", randomParameter, 1) - } - } else { - parsed = false - } +// Collect and merge/average all +func collectPong(pong *Pong) { + p, ok := Results[pong.Ping.Path] + if !ok { + p = Pongs{ + Path: pong.Ping.Path, + Method: pong.Ping.Method, } } - return basePath + path, parsed -} - -// Return a random string of the given length -func getRandString(length int) string { - b := make([]byte, length) - for i := range b { - b[i] = RandomStringCharset[seededRand.Intn(len(RandomStringCharset))] + if p.Urls == nil || regExParameterPattern.Match([]byte(pong.Ping.Path)) { + p.Urls = append(p.Urls, pong.Ping.Url) + p.Responses = append(p.Responses, pong.Response) } - return string(b) -} + p.Time += pong.Time + Results[pong.Ping.Path] = p -// Get the elapsed milliseconds from a given starting point -func getElapsedTimeInMS(start int64) int64 { - return (time.Now().UnixNano() - start) / int64(time.Millisecond) -} - -// isValidUrl tests a string to determine if it is a well-structured url or not. -func isValidUrl(toTest string) (*url.URL, bool) { - _, err := url.ParseRequestURI(toTest) - if err != nil { - return nil, false - } - - u, err := url.Parse(toTest) - if err != nil || u.Scheme == "" || u.Host == "" { - return nil, false - } - - return u, true -} - -// Contains a string in a slice -func contains(slice []string, val string) (int, bool) { - for i, item := range slice { - if item == val { - return i, true - } - } - return -1, false -} - -// If an error pops up, fail -func checkError(err error) { - if err != nil { - log.Fatal(err) - } + // Return to the source Neo + pongPool.Put(pong) } diff --git a/model.go b/model.go new file mode 100644 index 0000000..715a0e6 --- /dev/null +++ b/model.go @@ -0,0 +1,60 @@ +package main + +import ( + "sync" +) + +// A single entry to "ping" +type Ping struct { + Method string `json:"method"` + Path string `json:"path"` + Url string `json:"url"` + Headers map[string]string `json:"headers"` +} + +// A response +type Pong struct { + Ping Ping `json:"ping"` + Time int64 `json:"time"` + Response string `json:"response"` +} + +// All responses +type Pongs struct { + Path string `json:"path"` + Method string `json:"method"` + Time int64 `json:"time"` + Urls []string `json:"urls"` + Responses []string `json:"responses"` +} + +// Pre-parse the input to see if it is an openapi 3.0 or swagger 2.0 file +type SwaggerOpenApi struct { + Swagger string `json:"swagger;omitempty"` + OpenAPI string `json:"openapi;omitempty"` +} + +// The default request headers +var Headers = map[string]string{ + "Accept": "*/*", + "Connection": "Keep-Alive", + "Content-Type": "application/json", + "User-Agent": "aPing", +} + +// A pool of Ping objects to reduce the GC overhead +var pingPool = sync.Pool{ + New: func() interface{} { + return new(Ping) + }, +} + +// A pool of Pong objects to reduce the GC overhead +var pongPool = sync.Pool{ + New: func() interface{} { + return new(Pong) + }, +} + +// All collected Pongs +var Results = make(map[string]Pongs) diff --git a/output.go b/output.go new file mode 100644 index 0000000..e075f19 --- /dev/null +++ b/output.go @@ -0,0 +1,153 @@ +package main + +import ( + "encoding/json" + "github.com/jedib0t/go-pretty/table" + "io/ioutil" + "log" + "strings" + "time" +) + +var HtmlTemplate = ` + + + + + + + aPing - Results + + + + + + + + + + +

aPing - Results

+

{{TITLE}} @ {{DATE}}

+ +
+
+ {{TABLE}} +
+
+ + + + + + + + + + + + + + + +` + +// Result table collector +var ( + tableWriter table.Writer + tableColumnConfig = []table.ColumnConfig{ + {Name: "Path"}, + {Name: "URL"}, + {Name: "Method", WidthMax: 8}, + {Name: "Avg. ms"}, + {Name: "Response", WidthMax: 100}, + } +) + +// Flush all collected results to the aspired output +func flush(title string, output *string) { + // Create a table writer to log to + tableWriter = table.NewWriter() + tableWriter.SetAutoIndex(true) + tableWriter.AppendHeader(table.Row{"Path", "URL", "Method", "Avg. ms", "Response"}) + tableWriter.SetColumnConfigs(tableColumnConfig) + tableWriter.SetHTMLCSSClass("sort table table-striped table-hover table-responsive aping-table") + + // Flush the pongs + for _, result := range Results { + tableWriter.AppendRow(table.Row{ + result.Path, + strings.Join(result.Urls, "\r\n"), + result.Method, + result.Time / int64(*loopFlag), + strings.Join(result.Responses, "\r\n"), + }) + } + + // If an output file is given, write to it + if output != nil && *output != "" { + switch strings.ToLower(*output) { + case "console": + log.Println("\n" + tableWriter.Render()) + case "csv": + err := ioutil.WriteFile("aping.csv", []byte(tableWriter.RenderCSV()), 0644) + checkFatalError(err) + case "html": + html := strings.Replace(HtmlTemplate, "{{TITLE}}", title, 1) + html = strings.Replace(html, "{{DATE}}", time.Now().Format("2006-01-02 15:04:05"), 1) + html = strings.Replace(html, "{{TABLE}}", tableWriter.RenderHTML(), 1) + err := ioutil.WriteFile("aping.html", []byte(html), 0644) + checkFatalError(err) + case "md": + err := ioutil.WriteFile("aping.md", []byte(tableWriter.RenderMarkdown()), 0644) + checkFatalError(err) + case "json": + file, _ := json.MarshalIndent(Results, "", " ") + err := ioutil.WriteFile("aping.json", file, 0644) + checkFatalError(err) + } + } else { + // Otherwise just print the output + log.Println("\n" + tableWriter.Render()) + } +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..752dc46 --- /dev/null +++ b/util.go @@ -0,0 +1,60 @@ +package main + +import ( + "log" + "math/rand" + "net/url" + "time" +) + +// Seed +var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano())) + +// String charset to randomly pick from +const RandomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +// Return a random string of the given length +func getRandString(length int) string { + b := make([]byte, length) + for i := range b { + b[i] = RandomStringCharset[seededRand.Intn(len(RandomStringCharset))] + } + return string(b) +} + +// Get the elapsed milliseconds from a given starting point +func getElapsedTimeInMS(start int64) int64 { + return (time.Now().UnixNano() - start) / int64(time.Millisecond) +} + +// isValidUrl tests a string to determine if it is a well-structured url or not. +func isValidUrl(toTest string) (*url.URL, bool) { + _, err := url.ParseRequestURI(toTest) + if err != nil { + return nil, false + } + + u, err := url.Parse(toTest) + if err != nil || u.Scheme == "" || u.Host == "" { + return nil, false + } + + return u, true +} + +// Contains a string in a slice +func contains(slice []string, val string) (int, bool) { + for i, item := range slice { + if item == val { + return i, true + } + } + return -1, false +} + +// If a critical error pops up, fail +func checkFatalError(err error) { + if err != nil { + log.Fatal(err) + } +}