Collect the username when data scouting data is submitted

This patch adds the username of the person that is submitting scouting
data.

I kind of amended the existing unit test to validate this feature by
injecting a fake username at the right places. It doesn't validate
actual HTTPS traffic, but it's good enough for now.

Signed-off-by: Philipp Schrader <philipp.schrader@gmail.com>
Change-Id: I483cf30fd046965b23916b129a074906b586b096
diff --git a/scouting/webserver/requests/debug/cli/cli_test.py b/scouting/webserver/requests/debug/cli/cli_test.py
index f4b82b4..b20703b 100644
--- a/scouting/webserver/requests/debug/cli/cli_test.py
+++ b/scouting/webserver/requests/debug/cli/cli_test.py
@@ -91,7 +91,8 @@
             UpperGoalTele: (int32) 14,
             LowerGoalTele: (int32) 15,
             DefenseRating: (int32) 3,
-            Climbing: (int32) 1
+            Climbing: (int32) 1,
+            CollectedBy: (string) (len=9) "debug_cli"
             }"""), stdout)
 
     def test_request_all_matches(self):
diff --git a/scouting/webserver/requests/debug/debug.go b/scouting/webserver/requests/debug/debug.go
index 81be3d1..49b6f79 100644
--- a/scouting/webserver/requests/debug/debug.go
+++ b/scouting/webserver/requests/debug/debug.go
@@ -2,6 +2,7 @@
 
 import (
 	"bytes"
+	"encoding/base64"
 	"errors"
 	"fmt"
 	"io"
@@ -16,6 +17,9 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
 )
 
+// The username to submit the various requests as.
+const DefaultUsername = "debug_cli"
+
 // Use aliases to make the rest of the code more readable.
 type SubmitDataScoutingResponseT = submit_data_scouting_response.SubmitDataScoutingResponseT
 type RequestAllMatchesResponseT = request_all_matches_response.RequestAllMatchesResponseT
@@ -66,7 +70,16 @@
 // Performs a POST request with the specified payload. The bytes that the
 // server responds with are returned.
 func performPost(url string, requestBytes []byte) ([]byte, error) {
-	resp, err := http.Post(url, "application/octet-stream", bytes.NewReader(requestBytes))
+	req, err := http.NewRequest("POST", url, bytes.NewReader(requestBytes))
+	if err != nil {
+		log.Printf("Failed to create a new POST request to %s: %v", url, err)
+		return nil, err
+	}
+	req.Header.Add("Authorization", "Basic "+
+		base64.StdEncoding.EncodeToString([]byte(DefaultUsername+":")))
+
+	client := &http.Client{}
+	resp, err := client.Do(req)
 	if err != nil {
 		log.Printf("Failed to send POST request to %s: %v", url, err)
 		return nil, err
diff --git a/scouting/webserver/requests/messages/request_data_scouting_response.fbs b/scouting/webserver/requests/messages/request_data_scouting_response.fbs
index e30ab42..5703a07 100644
--- a/scouting/webserver/requests/messages/request_data_scouting_response.fbs
+++ b/scouting/webserver/requests/messages/request_data_scouting_response.fbs
@@ -11,6 +11,7 @@
     lower_goal_tele:int (id:7);
     defense_rating:int (id:8);
     climbing:int (id:9);
+    collected_by:string (id:10);
 }
 
 table RequestDataScoutingResponse {
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index 245891a..2076030 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -1,9 +1,11 @@
 package requests
 
 import (
+	"encoding/base64"
 	"errors"
 	"fmt"
 	"io"
+	"log"
 	"net/http"
 	"strconv"
 	"strings"
@@ -80,12 +82,44 @@
 	return result, success
 }
 
+// Parses the authorization information that the browser inserts into the
+// headers.  The authorization follows this format:
+//
+//  req.Headers["Authorization"] = []string{"Basic <base64 encoded username:password>"}
+func parseUsername(req *http.Request) string {
+	auth, ok := req.Header["Authorization"]
+	if !ok {
+		return "unknown"
+	}
+
+	parts := strings.Split(auth[0], " ")
+	if !(len(parts) == 2 && parts[0] == "Basic") {
+		return "unknown"
+	}
+
+	info, err := base64.StdEncoding.DecodeString(parts[1])
+	if err != nil {
+		log.Println("ERROR: Failed to parse Basic authentication.")
+		return "unknown"
+	}
+
+	loginParts := strings.Split(string(info), ":")
+	if len(loginParts) != 2 {
+		return "unknown"
+	}
+	return loginParts[0]
+}
+
 // Handles a SubmitDataScouting request.
 type submitDataScoutingHandler struct {
 	db Database
 }
 
 func (handler submitDataScoutingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	// Get the username of the person submitting the data.
+	username := parseUsername(req)
+	log.Println("Got data scouting data from", username)
+
 	requestBytes, err := io.ReadAll(req.Body)
 	if err != nil {
 		respondWithError(w, http.StatusBadRequest, fmt.Sprint("Failed to read request bytes:", err))
@@ -108,6 +142,7 @@
 		LowerGoalShots:  request.LowerGoalTele(),
 		PlayedDefense:   request.DefenseRating(),
 		Climbing:        request.Climbing(),
+		CollectedBy:     username,
 	}
 
 	err = handler.db.AddToStats(stats)
@@ -152,7 +187,7 @@
 
 	matches, err := handler.db.ReturnMatches()
 	if err != nil {
-		respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Faled to query database: ", err))
+		respondWithError(w, http.StatusInternalServerError, fmt.Sprint("Failed to query database: ", err))
 		return
 	}
 
@@ -281,6 +316,7 @@
 			LowerGoalTele:   stat.LowerGoalShots,
 			DefenseRating:   stat.PlayedDefense,
 			Climbing:        stat.Climbing,
+			CollectedBy:     stat.CollectedBy,
 		})
 	}
 
diff --git a/scouting/webserver/requests/requests_test.go b/scouting/webserver/requests/requests_test.go
index 60bee0e..0183736 100644
--- a/scouting/webserver/requests/requests_test.go
+++ b/scouting/webserver/requests/requests_test.go
@@ -222,12 +222,14 @@
 				ShotsMissed: 1, UpperGoalShots: 2, LowerGoalShots: 3,
 				ShotsMissedAuto: 4, UpperGoalAuto: 5, LowerGoalAuto: 6,
 				PlayedDefense: 7, Climbing: 8,
+				CollectedBy: "john",
 			},
 			{
 				TeamNumber: 972, MatchNumber: 1,
 				ShotsMissed: 2, UpperGoalShots: 3, LowerGoalShots: 4,
 				ShotsMissedAuto: 5, UpperGoalAuto: 6, LowerGoalAuto: 7,
 				PlayedDefense: 8, Climbing: 9,
+				CollectedBy: "andrea",
 			},
 		},
 	}
@@ -250,17 +252,20 @@
 			// MissedShotsAuto, UpperGoalAuto, LowerGoalAuto,
 			// MissedShotsTele, UpperGoalTele, LowerGoalTele,
 			// DefenseRating, Climbing,
+			// CollectedBy,
 			{
 				971, 1,
 				4, 5, 6,
 				1, 2, 3,
 				7, 8,
+				"john",
 			},
 			{
 				972, 1,
 				5, 6, 7,
 				2, 3, 4,
 				8, 9,
+				"andrea",
 			},
 		},
 	}