Commit a3b1b28

Richard Luby <richluby@gmail.com>
2016-11-09 09:15:10
server can give users test results
server accepts get requests for user tests. results are passed as array of ClientTests for client to display to user
1 parent f08eddd
Changed files (1)
serverHandlers.go
@@ -11,8 +11,14 @@ import (
 	"path/filepath"
 	"strconv"
 	"strings"
+	"sync"
 )
 
+// userTests is a map that contains arrays of client tests
+// this map is updated when a client requests a score
+var userTests map[string][]ClientTest
+var userTestsLock = sync.RWMutex{}
+
 // buildRecordArray builds an array of Records based on the parameter criteria
 func buildRecordArray(numQuestions int, categories []int) []Record {
 	var keyIndex, valueIndex int
@@ -174,6 +180,83 @@ func handleCategoryQueries(writer http.ResponseWriter, request *http.Request) {
 	}
 }
 
+// loadTestFile loads a user's test
+// in a parallel-safe way
+func loadTestFile(file *os.File, username string) error {
+	buffer, err := ioutil.ReadAll(file)
+	if err != nil {
+		return fmt.Errorf("Could not read test file: %+v\n", err.Error())
+	}
+	var test ClientTest
+	err = json.Unmarshal(buffer, &test)
+	if err != nil {
+		return fmt.Errorf("Could not parse test file %s: %+v\n", file.Name(), err.Error())
+	}
+	userTestsLock.Lock()
+	defer userTestsLock.Unlock()
+	if userTests[username] == nil {
+		userTests[username] = []ClientTest{}
+	}
+	userTests[username] = append(userTests[username], test)
+	return nil
+}
+
+// validateUserFile validates the existence of a user's test before attempting
+// to load it
+func validateUserFile(path string, fileInfo os.FileInfo, err error) error {
+	if !strings.Contains(path, serverConfig.USER_TESTS) {
+		return fmt.Errorf("Suspected malicious file access attempt: %s", path)
+	}
+	file, err := os.Open(path)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	fileStats, err := file.Stat()
+	if err != nil {
+		return err
+	}
+	if fileStats.Mode().IsRegular() {
+		directories := strings.Split(filepath.Dir(path), string(filepath.Separator))
+		username := directories[len(directories)-1]
+		err = loadTestFile(file, username)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// handleScoreRequests gives the client the scores for the specified user name
+func handleScoreRequests(writer http.ResponseWriter, request *http.Request) {
+	switch request.Method {
+	case "POST":
+		log.Printf("Client attempted to POST to /%s/test/score: %+v\tvia %+v", API_ROOT, request.RemoteAddr, request.UserAgent())
+	case "GET":
+		log.Printf("Client requested test score: %+v\tvia %+v", request.RemoteAddr, request.UserAgent())
+		username := request.FormValue("username")
+		if username != "" {
+			var err error
+			resultsFilePath := filepath.Join(serverConfig.USER_TESTS, username)
+			if err = filepath.Walk(resultsFilePath, validateUserFile); err != nil {
+				log.Printf("Failed to load user test due to error: %+v", err)
+				http.Error(writer, "Failed to load tests.", http.StatusInternalServerError)
+				return
+			}
+			userTestsLock.Lock()
+			defer userTestsLock.Unlock()
+			data, err := json.Marshal(userTests[username])
+			if err != nil {
+				log.Printf("Error for %s while marshaling: %+v", err, resultsFilePath)
+			}
+			writer.Write(data)
+			delete(userTests, username)
+		} else {
+			http.Error(writer, "No user specified.", http.StatusBadRequest)
+		}
+	}
+}
+
 type ServerHandler struct {
 	Request        string
 	HandleFunction func(writer http.ResponseWriter, request *http.Request)
@@ -186,8 +269,12 @@ func init() {
 		API_ROOT + "/test": ServerHandler{
 			Request:        API_ROOT + "/test",
 			HandleFunction: handleTestQueries},
+		API_ROOT + "/test/score": ServerHandler{
+			Request:        API_ROOT + "/test/score",
+			HandleFunction: handleScoreRequests},
 		API_ROOT + "/questions/categories": ServerHandler{
 			Request:        API_ROOT + "/questions/categories",
 			HandleFunction: handleCategoryQueries},
 	}
+	userTests = map[string][]ClientTest{}
 }