From dddd1b00a7f481ceae32e7cfcb86907eb77ee582 Mon Sep 17 00:00:00 2001 From: Paul Vaughan Date: Tue, 16 May 2023 22:09:22 +0100 Subject: [PATCH] Refactored the code to use workkers (10) to run multiple scans concurrently. --- main.go | 178 +++++++++++++++++++++++++++++++++++++++--------------- readme.md | 42 +++++++++---- 2 files changed, 158 insertions(+), 62 deletions(-) diff --git a/main.go b/main.go index 76c1c1b..43b8d98 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/cookiejar" "os" + "sort" "strings" "sync" "time" @@ -21,11 +22,19 @@ var urls = []string{ type Result int -type TLSResult struct { +type URLTestResult struct { tls10, tls11, tls12, tls13 Result tls10note, tls11note, tls12note, tls13note string } +const ( + resultUnknown Result = iota + resultSupportedOkay + resultSupportedNotOkay + resultUnsupportedOkay + resultUnsupportedNotOkay +) + var ( versionsStrings = map[uint16]string{ tls.VersionTLS10: "TLS 1.0", @@ -58,9 +67,16 @@ var ( resultUnsupportedNotOkay: `❌`, } - plural string + // Where we'll store all the results of the tests until we're ready to display it or write it out. + TestResults = make(map[string]URLTestResult) + TestResultsMutex = sync.RWMutex{} jar *cookiejar.Jar + fh *os.File + err error + + plural string + numJobs int ) const ( @@ -77,15 +93,11 @@ const ( testNotATLSHandshake = `first record does not look like a TLS handshake` testHTTPResponse = `server gave HTTP response to HTTPS client` - resultUnknown Result = iota - resultSupportedOkay - resultSupportedNotOkay - resultUnsupportedOkay - resultUnsupportedNotOkay + workers = 10 ) func main() { - fmt.Println("TLS Tester v0.2") + fmt.Println("TLS Tester v0.3") // Flags. url := flag.String(`url`, ``, `URL to test (e.g. 'google.com')`) @@ -94,45 +106,72 @@ func main() { urls = []string{*url} } + // Put the number of URLs to be tested into a variable as we use it in several places for output and channel size. + numJobs = len(urls) + // What we're doing, and pluralising the output if needs be (I *hate* e.g. '1 URLs'). - if len(urls) > 1 { + if numJobs > 1 { plural = `s` } - fmt.Printf("Checking %d URL%s for TLS security\n", len(urls), plural) + fmt.Printf("Checking %d URL%s for TLS security\n", numJobs, plural) startupTime := time.Now() - // Open a file for writing. - fh, err := os.Create(`./output.csv`) + // Open a file for writing now and quit early if it fails. + fh, err = os.Create(`./output.csv`) if err != nil { panic(err) } defer fh.Close() - // Writing the 'header' of the CSV file. - csvLine := fmt.Sprintf("URL,%s,%s,%s,%s,%s note,%s note,%s note,%s note\n", - versionsStrings[tls.VersionTLS10], versionsStrings[tls.VersionTLS11], versionsStrings[tls.VersionTLS12], versionsStrings[tls.VersionTLS13], - versionsStrings[tls.VersionTLS10], versionsStrings[tls.VersionTLS11], versionsStrings[tls.VersionTLS12], versionsStrings[tls.VersionTLS13], - ) - _, err = fh.WriteString(csvLine) - if err != nil { - panic(err) - } - // Set up a new cookie jar (some websites fail if a cookie cannot be written and then read back). jar, err = cookiejar.New(nil) if err != nil { panic(err) } - for i, url := range urls { - fmt.Printf("% 5d: %50s:\t\t", i+1, url) + // Set up the jobs and results channels. + jobs := make(chan string, numJobs) + results := make(chan string, numJobs) + + // Spin up some workers. + for w := 1; w <= workers; w++ { + go processURL(w, jobs, results) + } + + // Send the jobs. + for _, url := range urls { + jobs <- url + } + close(jobs) + + // Receive the results. + for a := 1; a <= numJobs; a++ { + <-results + } + + fmt.Printf("Processing complete.\n\n") + + // Write out the data to screen, CSV, wherever. + var wg sync.WaitGroup + + wg.Add(2) + writeScreen(TestResults, &wg) + writeCSV(TestResults, &wg) + wg.Wait() + + fmt.Printf("Done. Tested %d URL%s in %s.\n", numJobs, plural, time.Since(startupTime).Round(time.Millisecond)) +} + +func processURL(id int, jobs <-chan string, results chan<- string) { + for url := range jobs { + fmt.Printf(" Processing %s...\n", url) // Set up a waitgroup for the four TLS version tests. var wg sync.WaitGroup // Set defaults for the output. - testResult := TLSResult{resultUnknown, resultUnknown, resultUnknown, resultUnknown, ``, ``, ``, ``} + testResult := URLTestResult{resultUnknown, resultUnknown, resultUnknown, resultUnknown, ``, ``, ``, ``} for _, tlsVersion := range []uint16{tls.VersionTLS10, tls.VersionTLS11, tls.VersionTLS12, tls.VersionTLS13} { wg.Add(1) @@ -142,33 +181,15 @@ func main() { // Wait for all the tests to complete. wg.Wait() - // Basic 'tick', 'cross' or 'unknown' output for the screen. - fmt.Printf("%s %s %s %s\n", resultsIcons[testResult.tls10], resultsIcons[testResult.tls11], resultsIcons[testResult.tls12], resultsIcons[testResult.tls13]) - - // Create one line of the CSV file from the results and notes of this URL's test. - csvLine := fmt.Sprintf("%s,%s,%s,%s,%s,%s,%s,%s,%s\n", - url, - resultsShortStrings[testResult.tls10], - resultsShortStrings[testResult.tls11], - resultsShortStrings[testResult.tls12], - resultsShortStrings[testResult.tls13], - testResult.tls10note, - testResult.tls11note, - testResult.tls12note, - testResult.tls13note, - ) + TestResultsMutex.Lock() + TestResults[url] = testResult + TestResultsMutex.Unlock() - // Write this URL's tests out to the CSV file. - _, err := fh.WriteString(csvLine) - if err != nil { - panic(err) - } + results <- url } - - fmt.Printf("Done. Tested %d URL%s in %s.\n", len(urls), plural, time.Since(startupTime).Round(time.Millisecond)) } -func testTLSVersion(tlsVersion uint16, url string, testResult *TLSResult, wg *sync.WaitGroup) { +func testTLSVersion(tlsVersion uint16, url string, testResult *URLTestResult, wg *sync.WaitGroup) { defer wg.Done() var ( @@ -178,7 +199,7 @@ func testTLSVersion(tlsVersion uint16, url string, testResult *TLSResult, wg *sy client := &http.Client{ Jar: jar, - Timeout: 3 * time.Second, + Timeout: 15 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{ MinVersion: tlsVersion, @@ -280,3 +301,62 @@ func testTLSVersion(tlsVersion uint16, url string, testResult *TLSResult, wg *sy testResult.tls13note = tlsURLNote } } + +// Basic 'tick', 'cross' or 'unknown' output for the screen. +func writeScreen(results map[string]URLTestResult, wg *sync.WaitGroup) { + defer wg.Done() + + // Sort the data via the keys. + keys := make([]string, 0, len(results)) + for key := range results { + keys = append(keys, key) + } + sort.Strings(keys) + + for i, key := range keys { + fmt.Printf("% 5d: %50s:\t\t%s %s %s %s\n", i+1, key, resultsIcons[results[key].tls10], resultsIcons[results[key].tls11], resultsIcons[results[key].tls12], resultsIcons[results[key].tls13]) + } +} + +func writeCSV(results map[string]URLTestResult, wg *sync.WaitGroup) { + defer wg.Done() + + // Writing the 'header' of the CSV file. + csvLine := fmt.Sprintf("URL,%s,%s,%s,%s,%s note,%s note,%s note,%s note\n", + versionsStrings[tls.VersionTLS10], versionsStrings[tls.VersionTLS11], versionsStrings[tls.VersionTLS12], versionsStrings[tls.VersionTLS13], + versionsStrings[tls.VersionTLS10], versionsStrings[tls.VersionTLS11], versionsStrings[tls.VersionTLS12], versionsStrings[tls.VersionTLS13], + ) + _, err := fh.WriteString(csvLine) + if err != nil { + panic(err) + } + + keys := make([]string, 0, len(results)) + for key := range results { + keys = append(keys, key) + } + sort.Strings(keys) + + // Create one line of the CSV file from the results and notes of this URL's test. + for _, key := range keys { + // fmt.Printf("% 5d: %50s:\t\t%s %s %s %s\n", i+1, key, resultsIcons[results[key].tls10], resultsIcons[results[key].tls11], resultsIcons[results[key].tls12], resultsIcons[results[key].tls13]) + + csvLine = fmt.Sprintf("%s,%s,%s,%s,%s,%s,%s,%s,%s\n", + key, + resultsShortStrings[results[key].tls10], + resultsShortStrings[results[key].tls11], + resultsShortStrings[results[key].tls12], + resultsShortStrings[results[key].tls13], + results[key].tls10note, + results[key].tls11note, + results[key].tls12note, + results[key].tls13note, + ) + + // Write this URL's tests out to the CSV file. + _, err = fh.WriteString(csvLine) + if err != nil { + panic(err) + } + } +} diff --git a/readme.md b/readme.md index dc1afea..b84de53 100644 --- a/readme.md +++ b/readme.md @@ -19,13 +19,16 @@ TLS Tester connects to a given URL with all four versions of TLS and logs the re ## Using a pre-built binary 1. [Download a binary](https://github.com/vaughany/tls-tester/releases) for Linux or Windows. -2. Run it with e.g. `./tls-tester` or `tls-tester.exe` +2. Make it executable + * Linux: `chmod +x tls-tester` + * Windows: right-click on it, Properties, Unblock (IIRC). +3. Run it with e.g. `./tls-tester` or `tls-tester.exe` --- ## Command-line Options -By default, the program will scan four common URLs. Run the command with `-url your-domain.com` to scan a URL of your choice. +By default, the program will scan four common URLs, purely for example. Run the command with `-url your-domain.com` to scan a URL of your choice. Run the command with `-h` for help. @@ -38,17 +41,29 @@ Typical on-screen output looks like this, with the URL being tested and the resu Remember that ideally, we want to see TLS 1.2 or 1.3, and **do not want** to see TLS 1.0 or 1.1. ```bash -TLS Tester v0.1 +TLS Tester v0.3 Checking 4 URLs for TLS security - 1: apple.com: ✅ ✅ ✅ ✅ - 2: facebook.com: ❌ ❌ ✅ ✅ - 3: google.com: ❌ ❌ ✅ ✅ - 4: twitter.com: ✅ ✅ ✅ ✅ - | | | | - | | | +- TLS 1.3 - | | +---- TLS 1.2 - | +------- TLS 1.1 - +---------- TLS 1.0 + Processing apple.com... + Processing facebook.com... + Processing google.com... + Processing twitter.com... +Processing complete. + + 1: apple.com: ✅ ✅ ✅ ✅ + 2: facebook.com: ❌ ❌ ✅ ✅ + 3: google.com: ❌ ❌ ✅ ✅ + 4: twitter.com: ✅ ✅ ✅ ✅ +Done. Tested 4 URLs in 533ms. +``` + +The icons indicate: + +```bash + | | | | + | | | +- TLS 1.3 + | | +---- TLS 1.2 + | +------- TLS 1.1 + +---------- TLS 1.0 ``` * If TLS 1.0 or 1.1 are in use, a ❌ is shown in that column. @@ -99,9 +114,10 @@ This is a basic tool, reporting only basic results. The results are neither defi * **2022-05-15, v0.1:** initial release. Scans four common URLs by default or use `-url` to scan a URL of your choice. * **2022-05-15, v0.2:** used goroutines and waitgroups to run the four TLS version tests concurrently-per-URL, significantly reducing the testing time. +* **2022-05-16, v0.3:** refactored the code to use multiple (10) workers / queues, so the process can be completed much faster if scanning many URLs. --- ## To-Do -1. Refactor the code to use multiple workers / queues, so the process can be completed much faster if scanning many URLs. +1. Percentages of TLS version use