Skip to content

Commit

Permalink
Added ability to use pre-shared key or data will be rejected; also ra…
Browse files Browse the repository at this point in the history
…ndom key generating helper function.
  • Loading branch information
vaughany committed Sep 10, 2021
1 parent b691f09 commit dd85d50
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 25 deletions.
65 changes: 57 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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
```

---

Expand Down Expand Up @@ -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.
Expand All @@ -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:

Expand All @@ -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).

---

Expand All @@ -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.

---

Expand Down
25 changes: 19 additions & 6 deletions cmd/collector/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main

import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
Expand All @@ -24,6 +25,10 @@ import (
vcms "vcms/internal"
)

type (
contextKey string
)

func main() {
const (
cmdName string = "VCMS - Collector"
Expand All @@ -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()

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down
14 changes: 12 additions & 2 deletions cmd/receiver/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand Down
27 changes: 20 additions & 7 deletions cmd/receiver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)

Expand Down Expand Up @@ -66,6 +65,11 @@ type rowData struct {
DiskFree template.HTML
}

type ContextHandler struct {
debug bool
APIKey string
}

func main() {
const (
cmdName = "VCMS - Receiver"
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions internal/vcms.go
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit dd85d50

Please sign in to comment.