Commit 1937445

Richard Luby <richluby@gmail.com>
2017-01-27 07:11:28
server accepts submitted questions
server uses TOTP, 6 digit token with a 30s period and 1 shift window to provide authorization to post questions. token is passed as URL parameter in token variable. questions are expected as JSON-formatted array of Records.
1 parent 06fa1d0
serverHandlers.go
@@ -1,8 +1,10 @@
 package main
 
 import (
+	"encoding/base32"
 	"encoding/json"
 	"fmt"
+	"github.com/pquerna/otp/totp"
 	"io/ioutil"
 	"log"
 	"math/rand"
@@ -12,6 +14,7 @@ import (
 	"strconv"
 	"strings"
 	"sync"
+	"time"
 )
 
 // userTests is a map that contains arrays of client tests
@@ -291,6 +294,57 @@ func handleScoreRequests(writer http.ResponseWriter, request *http.Request) {
 	}
 }
 
+// parseReceivedQuestions takes the questions from the client submission
+// and adds them to the database. The server expects a JSON-formatted array
+// of Records. There should be a TOKEN in the url. This TOKEN is a six-digit,
+// TOTP token based on SUBMISSION_SECRET.
+func parseReceivedQuestions(writer http.ResponseWriter, request *http.Request) {
+	token := request.FormValue("token")
+	correctToken, err := totp.GenerateCode(base32.StdEncoding.EncodeToString([]byte(SUBMISSION_SECRET)),
+		time.Now())
+	if err != nil {
+		log.Printf("Error generating token: %+v", err)
+	}
+	if token != correctToken {
+		http.Error(writer, "Invalid token.", http.StatusForbidden)
+		log.Printf("Invalid token from: %+v\tvia %+v\tis: %s, should be: %s", request.RemoteAddr,
+			request.UserAgent(), token, correctToken)
+		return
+	}
+	var newQuestions []Record
+	data, err := ioutil.ReadAll(request.Body)
+	if err != nil {
+		http.Error(writer, "Could not read the question submission.", http.StatusBadRequest)
+		log.Printf("Error for connection %s while reading questions: %+v", request.RemoteAddr, err)
+		return
+	}
+	// check to make sure properly formatted client response
+	err = json.Unmarshal(data, &newQuestions)
+	if err != nil {
+		http.Error(writer, "Could not parse the question submission.", http.StatusBadRequest)
+		log.Printf("Error while parsing submitted questions: %+v", err)
+		return
+	}
+	for _, record := range newQuestions {
+		err = addRecordToDB(&record)
+		if err != nil {
+			log.Printf("Could not add\n\t%+v\nto DB due to: %+v", err)
+		}
+	}
+}
+
+// handleQuestionQueries handles the submissions for questions
+func handleQuestionQueries(writer http.ResponseWriter, request *http.Request) {
+	defer request.Body.Close()
+	switch request.Method {
+	case "POST":
+		log.Printf("Client attempted to POST to %s/questions: %+v\tvia %+v", API_ROOT, request.RemoteAddr, request.UserAgent())
+		parseReceivedQuestions(writer, request)
+	case "GET":
+		log.Printf("Client attempted to GET from %s/questions: %+v\tvia %+v", API_ROOT, request.RemoteAddr, request.UserAgent())
+	}
+}
+
 type ServerHandler struct {
 	Request        string
 	HandleFunction func(writer http.ResponseWriter, request *http.Request)
@@ -309,6 +363,9 @@ func init() {
 		API_ROOT + "/questions/categories": ServerHandler{
 			Request:        API_ROOT + "/questions/categories",
 			HandleFunction: handleCategoryQueries},
+		API_ROOT + "/questions": ServerHandler{
+			Request:        API_ROOT + "/questions",
+			HandleFunction: handleQuestionQueries},
 	}
 	userTests = map[string][]ClientTest{}
 }
structures.go
@@ -7,6 +7,9 @@ import (
 // API_ROOT defines the root path for the web api interface
 const API_ROOT = "/api"
 
+// SUBMISSION_SECRET holds the secret for submitting questions
+const SUBMISSION_SECRET = "shadows"
+
 // mapLock locks the maps to prevent concurrency issues
 var mapLock *sync.Mutex