From dd85d502d5fb6c1092e7d225206028aaf20df51e Mon Sep 17 00:00:00 2001 From: Paul Vaughan Date: Fri, 10 Sep 2021 22:34:59 +0100 Subject: [PATCH] Added ability to use pre-shared key or data will be rejected; also random key generating helper function. --- README.md | 65 +++++++++++++++++++++++++++++++++++++------ cmd/collector/main.go | 25 +++++++++++++---- cmd/receiver/api.go | 14 ++++++++-- cmd/receiver/main.go | 27 +++++++++++++----- internal/vcms.go | 5 ++-- 5 files changed, 111 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index bda93d2..84db1d7 100644 --- a/README.md +++ b/README.md @@ -176,9 +176,9 @@ I aim to test both Receiver and Collector programs on as many OSes and architect ## Better Linux installation -By default, the Receiver runs the web page and API on `127.0.0.1` ('localhost') on port `8080`. These are fair defaults, but won't work outside your computer. +By default, the Receiver runs the web page and API on `127.0.0.1` ('localhost') on port `8080`. These are fair defaults, but won't work outside your computer. Both programs allow you to specify the IP address and port to use with the `-r` flag. -Both programs allow you to specify the IP address and port to use with the `-r` flag. +As of v0.0.10 you can force the use of a pre-shared key, so that data sent without it is rejected. There's also a helper function to generate one for you. 1. To run Receiver on a different IP address and/or port: @@ -197,11 +197,57 @@ Both programs allow you to specify the IP address and port to use with the `-r` ./collector -r http://192.168.0.100:8081 ``` -> **Note:** Some TCP/IP ports, such as the standard web port 80, are restricted and cannot be used unless you run the program with elevated privileges: -> ->`./receiver -r 192.168.0.100:80 // reserved port: fails.` -> ->`sudo ./receiver -r 192.168.0.100:80 // works.` + > **Note:** Some TCP/IP ports, such as the standard web port 80, are restricted and cannot be used unless you run the program with elevated privileges: + > + >`./receiver -r 192.168.0.100:80 // reserved port: fails.` + > + >`sudo ./receiver -r 192.168.0.100:80 // works.` + +3. To force the use of a pre-shared key for added security: + + If the Receiver specifies a pre-shared (API) key, then any Collector sending data is required to send the same key within the JSON, or the data will be rejected. + + > **Note:** The Receiver is not yet using HTTPS so data is not encrypted in transit, nor is the API key encrypted in any way in the JSON. + + Run the Receiver as follows (continuing the example from above): + + ``` + ./receiver -r 192.168.0.100:8081 --apikey long-string-of-letters-and-numbers + ``` + + > **Note:** You can't specify **no** API key (e.g. `--apikey`) but you can specify an **empty** API key (e.g. `--apikey ""`), which is treated as no API key. This is silly, don't do it. + + At startup, the helper text shown to run a correcly-configured Collector will show the new command: + + ``` + To connect a Collector, run: './collector -r http://192.168.0.100:8081 --apikey long-string-of-letters-and-numbers'. + ``` + + If a Collector has not been similarly configured, you'll get an error message and HTTP status code 403: + + ``` + Response: 403 Forbidden + ERROR: 192.168.0.100:8081: Collector API key '' does not match Receiver API key (not shown). Ignoring data. + ``` + + ...so configure the Collector as described above. + +4. Getting a random pre-shared key: + + Both Receiver and Collector apps can be run with `-k` as the only argument, and this will generate a handful of keys of varying lengths in both hex and base64 formats, then quit. It's just a convenience helper, nothing more. Run it multiple times for more random keys. + + ``` + $ ./collector -k + Hex: + 16 chars: 1869abd649331542 / 1869ABD649331542 + 32 chars: 2c282ff203bb30879a5505493cbc2d13 / 2C282FF203BB30879A5505493CBC2D13 + 48 chars: bb12c3223fa26ed0b04f7767758f48a3c42bfbc07c07d826 / BB12C3223FA26ED0B04F7767758F48A3C42BFBC07C07D826 + Base64: + 16 chars: i0L2oZt32HSIKWhC + 32 chars: Pjb3niW4La7OsTcuJeKI8PPYeinBjhBn + 48 chars: nBqtfU-gdvZ74XZP3phTv7CE1jezCFjVfkajfYofU7V8B2Bs + 64 chars: eS5uz5UyV5x31T_aRYEHWHRC3Jj5dtDErPmjN3SqOOI9MVPKUBmiQ4HYazy37Nef + ``` --- @@ -287,7 +333,6 @@ There's a lot I want to do: * Choice of HTTP or HTTPS. * Create a proper web-app with secure login. * Create groups to group Collectors by, e.g. 'dev', 'production' etc. -* Authentication of Collectors by e.g. pre-shared key. * More active monitoring, with alerts should something go awry. * Web-based, Email and Slack notifications of alerts. * Monitor software deployed by the Ruby gem Capistrano. @@ -302,6 +347,7 @@ There's a lot I want to do: * Consider using github.com/shirou/gopsutil for OS details. * Look into using gRPC / protocol buffers. Unsure if they have any advantage over simple JSON. * Send a 'pause', 'restart' or 'force send' from the Receiver. +* Controls on the Receiver web page to increase / decrease the speed of individual Collector or pause it for a bit / a lot. Completed to-do's: @@ -310,6 +356,8 @@ Completed to-do's: * Ability to remove a node from the Receiver (mostly this was for testing, so I didn't have to quit and restart it) (v0.0.5). * Export as JSON (v0.0.7). * Page listing just the hosts (v0.0.8). +* Logging to a file as well as stdout (v0.0.9). +* Authentication of Collectors by pre-shared key (v0.0.10). --- @@ -324,6 +372,7 @@ Completed to-do's: * **2021-08-08**, v0.0.7. Added ability to export all data as JSON. * **2021-09-07**, v0.0.8. Lots of changes based on comments from the nice people at the Gophers Slack, including: removing init(), removing globals (some: work in progress), cleaning up the readme, adding OS logos, using GoReleaser and more linters, basic tests and benchmarks (also work in progress), new lighter dashboard and hosts web pages, and tidied up the struct used to marshal/unmarshal data. * **2021-09-09**, v0.0.9. Added logging to a file. Creates folder in cwd, then creates a named and dated file, logs to it and stdout. +* **2021-09-10**, v0.0.10. Added a basic form of authentication by the use of pre-shared keys, using the `--apikey` flag. Collectors sending data with a non-matching or empty key will have their data rejected. Generate random keys with the `-k` helper flag. --- diff --git a/cmd/collector/main.go b/cmd/collector/main.go index 030f536..f21df67 100644 --- a/cmd/collector/main.go +++ b/cmd/collector/main.go @@ -3,6 +3,7 @@ package main import ( "bytes" + "context" "encoding/json" "errors" "flag" @@ -24,6 +25,10 @@ import ( vcms "vcms/internal" ) +type ( + contextKey string +) + func main() { const ( cmdName string = "VCMS - Collector" @@ -38,12 +43,14 @@ func main() { testing = false receiverURL = "http://127.0.0.1:8080" logName = vcms.CreateLogName(vcms.LogFolder, cmdCodename) + APIKey = "" ) - flag.BoolVar(&debug, "d", debug, "Shows debugging info") + flag.StringVar(&APIKey, "apikey", APIKey, "API key, if the Receiver requires one") + flag.BoolVar(&debug, "d", debug, "Shows debugging info, incl all JSON being sent") flag.BoolVar(&keyGen, "k", false, "Quickly generate a few random keys") - flag.BoolVar(&testing, "t", testing, "Creates a random hostname, username and IP address") flag.StringVar(&receiverURL, "r", receiverURL, "URL of the 'Receiver' application") + flag.BoolVar(&testing, "t", testing, "Creates a random hostname, username and IP address") flag.BoolVar(&version, "v", version, "Show version info and quit") flag.Parse() @@ -75,10 +82,15 @@ func main() { log.Printf("%s \n", cmdDesc) log.Printf("%s \n", vcms.AppDesc) - sendAnnounce(debug, testing, receiverURL) + ctx := context.Background() + ctx = context.WithValue(ctx, contextKey("debug"), debug) + ctx = context.WithValue(ctx, contextKey("testing"), testing) + ctx = context.WithValue(ctx, contextKey("APIKey"), APIKey) + + sendAnnounce(ctx, contextKey("debug"), contextKey("testing"), contextKey("APIKey"), receiverURL) } -func sendAnnounce(debug bool, testing bool, receiverURL string) { +func sendAnnounce(ctx context.Context, debug contextKey, testing contextKey, APIKey contextKey, receiverURL string) { var ( watchDelay = 10 startTime = time.Now() @@ -113,9 +125,10 @@ func sendAnnounce(debug bool, testing bool, receiverURL string) { data.Meta.AppVersion = vcms.AppVersion data.Meta.AppUptime = getAppUptime(startTime) data.Meta.Errors = errors + data.Meta.APIKey = fmt.Sprintf("%s", ctx.Value(APIKey)) // Adjust some of the core data if we're testing. - if testing { + if ctx.Value(testing) == true { data.Hostname = getRandomHostname() data.IPAddress = getRandomIPAddress() data.Username = getRandomUsername() @@ -127,7 +140,7 @@ func sendAnnounce(debug bool, testing bool, receiverURL string) { } sendURL := receiverURL + "/api/announce" - if debug { + if ctx.Value(debug) == true { log.Printf("Sending %s to %s", jsonBytes, sendURL) } else { log.Printf("Sending data to %s", sendURL) diff --git a/cmd/receiver/api.go b/cmd/receiver/api.go index 750c6b7..9f8eb4c 100644 --- a/cmd/receiver/api.go +++ b/cmd/receiver/api.go @@ -10,13 +10,13 @@ import ( vcms "vcms/internal" ) -func apiAnnounceHandler(w http.ResponseWriter, r *http.Request) { +func (ch *ContextHandler) apiAnnounceHandler(w http.ResponseWriter, r *http.Request) { jsonBytes, err := io.ReadAll(r.Body) if err != nil { log.Panic(err) } - if debug { + if ch.debug { log.Printf("Received %s from %s", jsonBytes, r.RemoteAddr) } else { log.Printf("Received data from %s", r.RemoteAddr) @@ -37,6 +37,16 @@ func apiAnnounceHandler(w http.ResponseWriter, r *http.Request) { return } + // Check for API key, ensure it matches if length is non-zero. + if len(ch.APIKey) > 0 { + if data.Meta.APIKey != ch.APIKey { + error := fmt.Sprintf("ERROR: %s: Collector API key '%s' does not match Receiver API key (not shown). Ignoring data.", r.Host, data.Meta.APIKey) + log.Println(error) + http.Error(w, error, http.StatusForbidden) + return + } + } + // It's all good. w.WriteHeader(200) diff --git a/cmd/receiver/main.go b/cmd/receiver/main.go index 13dc309..8d832ec 100644 --- a/cmd/receiver/main.go +++ b/cmd/receiver/main.go @@ -25,7 +25,6 @@ var embeddedFiles embed.FS // Something like this, to ditch global state? // https://stackoverflow.com/a/46517000/254146 var ( - debug = false nodes = make(map[string]*vcms.SystemData) ) @@ -66,6 +65,11 @@ type rowData struct { DiskFree template.HTML } +type ContextHandler struct { + debug bool + APIKey string +} + func main() { const ( cmdName = "VCMS - Receiver" @@ -75,16 +79,19 @@ func main() { ) var ( + debug = false version = false keyGen = false receiverURL = "127.0.0.1:8080" // Don't put e.g. http:// at the start. Add this to docs. logName = vcms.CreateLogName(vcms.LogFolder, cmdCodename) + APIKey = "" ) - flag.BoolVar(&debug, "d", debug, "Shows debugging info") - flag.BoolVar(&version, "v", false, "Show version info and quit") + flag.StringVar(&APIKey, "apikey", APIKey, "API key, if you want the Collector to prove it's legit") + flag.BoolVar(&debug, "d", debug, "Shows debugging info, incl all JSON being sent") flag.BoolVar(&keyGen, "k", false, "Quickly generate a few random keys") flag.StringVar(&receiverURL, "r", receiverURL, "URL to run this application's web server on") + flag.BoolVar(&version, "v", false, "Show version info and quit") flag.Parse() if version { @@ -116,7 +123,7 @@ func main() { log.Printf("%s \n", vcms.AppDesc) if debug { - go dumper(nodes) + go dumper() } // Load the nodes from a file. @@ -141,7 +148,9 @@ func main() { http.HandleFunc("/dashboard/full", dashboardHandler) http.HandleFunc("/hosts", hostsHandler) http.HandleFunc("/host/", hostHandler) // Note the trailing '/'. - http.HandleFunc("/api/announce", apiAnnounceHandler) + + contextHandler := &ContextHandler{debug: debug, APIKey: APIKey} + http.HandleFunc("/api/announce", contextHandler.apiAnnounceHandler) http.HandleFunc("/api/ping", apiPingHandler) http.HandleFunc("/save", saveToPersistentStorageHandler) @@ -150,12 +159,16 @@ func main() { http.HandleFunc("/export/json", exportJSONHandler) + APIKeyInfo := "" + if len(APIKey) > 0 { + APIKeyInfo = fmt.Sprintf(" --apikey %s", APIKey) + } log.Printf("Running web server on http://%s.", receiverURL) - log.Printf("To connect a Collector, run: './collector -r http://%s'.", receiverURL) + log.Printf("To connect a Collector, run: './collector -r http://%s%s'.", receiverURL, APIKeyInfo) log.Fatal(http.ListenAndServe(receiverURL, nil)) } -func dumper(nodes map[string]*vcms.SystemData) { +func dumper() { for { log.Println("Dumping nodes:") if len(nodes) > 0 { diff --git a/internal/vcms.go b/internal/vcms.go index 5dd1d61..5840ce9 100644 --- a/internal/vcms.go +++ b/internal/vcms.go @@ -18,8 +18,8 @@ AppXxx stores strings common to both the Collector and Receiver. ProjectURL is the URL of the project on GitHub. */ const ( - AppDate string = "2021-09-09" - AppVersion string = "0.0.9" + AppDate string = "2021-09-10" + AppVersion string = "0.0.10" AppTitle string = "Vaughany's Computer Monitoring System" ProjectURL string = "github.com/vaughany/vcms" AppDesc string = "Description of the whole system goes here." @@ -52,6 +52,7 @@ type Meta struct { AppVersion string `json:"app_version"` AppUptime string `json:"app_uptime"` Errors []string `json:"errors"` + APIKey string `json:"api_key"` } // CPU struct holds details about the CPU.