master
Raw Download raw file
  1package main
  2
  3import (
  4	"encoding/base32"
  5	"encoding/json"
  6	"fmt"
  7	"github.com/pquerna/otp/totp"
  8	"io/ioutil"
  9	"log"
 10	"math/rand"
 11	"net/http"
 12	"os"
 13	"path/filepath"
 14	"strconv"
 15	"strings"
 16	"sync"
 17	"time"
 18)
 19
 20// userTests is a map that contains arrays of client tests
 21// this map is updated when a client requests a score
 22var userTests map[string][]ClientTest
 23var userTestsLock = sync.RWMutex{}
 24
 25// randomizeArray takes an array and returns the shuffled version
 26// following the Fisher-Yates shuffle
 27func randomizeArray(array []Record) []Record {
 28	var tempRecord Record
 29	maxIndex := len(array)
 30	currentIndex := maxIndex
 31	for currentIndex != 0 {
 32		randomIndex := rand.Intn(currentIndex)
 33		currentIndex -= 1
 34		tempRecord = array[currentIndex]
 35		array[currentIndex] = array[randomIndex]
 36		array[randomIndex] = tempRecord
 37	}
 38	return array
 39}
 40
 41// buildRecordArray builds an array of Records based on the parameter criteria
 42// will return numQuestions, or the available questions, whichever is less
 43func buildRecordArray(numQuestions int, selectedCategories []string) ([]Record, error) {
 44	recordArray := []Record{}
 45	var err error
 46	if len(selectedCategories) > 0 {
 47		recordArray, err = returnForQuery(selectedCategories)
 48	} else {
 49		recordArray, err = returnForQuery(categories)
 50	}
 51	recordArray = randomizeArray(recordArray)
 52	if len(recordArray) > numQuestions {
 53		return recordArray[0:numQuestions], err
 54	}
 55	return recordArray, err
 56}
 57
 58// parseBluePrint takes a list of category indices and returns
 59// the chosen string representation of the categories
 60func parseBluePrint(blueprint string) ([]string, error) {
 61	splits := strings.Split(blueprint, "-")
 62	chosenCategories := make([]string, len(splits))
 63	for i, key := range splits {
 64		tempIndex, err := strconv.Atoi(strings.TrimSpace(key))
 65		if err != nil {
 66			return nil, fmt.Errorf("Error parsing numbers: %+v", err)
 67		}
 68		if tempIndex < len(categories) {
 69			chosenCategories[i] = categories[tempIndex]
 70		} else {
 71			return nil, fmt.Errorf("Selected index too large for categories: %d", tempIndex)
 72		}
 73	}
 74	return chosenCategories, nil
 75}
 76
 77// handleRequestForTest provides a JSON formatted test for the client
 78func handleRequestForTest(writer http.ResponseWriter, request *http.Request) error {
 79	var giveRecords []Record
 80	var err error
 81	var chosenCategories []string
 82	questions := request.FormValue("questions")
 83	blueprint := request.FormValue("blueprint")
 84	if blueprint != "" {
 85		chosenCategories, err = parseBluePrint(blueprint)
 86		if err != nil {
 87			http.Error(writer, "Improperly formatted blueprint parameter.", http.StatusBadRequest)
 88			return fmt.Errorf("Error building blueprint: %+v", err)
 89		}
 90	}
 91	numQuestions, err := strconv.Atoi(questions)
 92	if err != nil {
 93		http.Error(writer, "Improperly formatted question parameter.", http.StatusBadRequest)
 94		return fmt.Errorf("Error building questions: %+v", err)
 95	}
 96	giveRecords, err = buildRecordArray(numQuestions, chosenCategories)
 97	if err != nil {
 98		http.Error(writer, "Failed to generate record array.", http.StatusInternalServerError)
 99		return fmt.Errorf("Error while retrieving records from DB: %+v", err)
100	}
101	data, err := json.Marshal(giveRecords)
102	if err != nil {
103		http.Error(writer, "Failed to generate record array.", http.StatusInternalServerError)
104		return fmt.Errorf("Error building questions: %+v", err)
105	}
106	writer.Write(data)
107	log.Printf("Gave %d questions to %s", len(giveRecords), request.RemoteAddr)
108	return nil
109}
110
111// writeTestFile writes the the data to the given writer for the specified username
112// files are written in the format <numQuestions>-<score>-<id>
113func writeTestFile(clientTest ClientTest, data []byte) error {
114	var err error
115	resultsFilePath := filepath.Join(serverConfig.USER_TESTS, clientTest.Username)
116	if err = os.MkdirAll(resultsFilePath, 0700); err != nil {
117		log.Printf("Could not create test directory for user %s: %+v", clientTest.Username, err)
118		return err
119	}
120	resultsFilePath = filepath.Join(resultsFilePath,
121		strconv.Itoa(len(clientTest.Records))+
122			"-"+fmt.Sprintf("%.2f", clientTest.Score))
123	id := fmt.Sprintf("%05d", rand.Intn(99999)) // generate random 5-digit id
124	err = ioutil.WriteFile(resultsFilePath+"-"+id, data, 0400)
125	return err
126}
127
128// validateScore verifies that the user test score is valid
129// if not, the score is recalculated using integer math
130func validateScore(clientTest *ClientTest) {
131	if clientTest.Score <= 0 || clientTest.Score > 100 {
132		clientTest.Score = 0
133		for _, record := range clientTest.Records {
134			if record.AnsweredCorrectly == true {
135				clientTest.Score++
136			}
137		}
138		clientTest.Score = clientTest.Score / float32(len(clientTest.Records)) * 100
139	}
140}
141
142// handlePostingTest receives a client's test results, and then stores them
143func handlePostingTest(writer http.ResponseWriter, request *http.Request) error {
144	if request.URL.Path != handlers[API_ROOT+"/test"].Request {
145		return fmt.Errorf("Incorrectly formatted URL path: %+v", request.URL.Path)
146	}
147	var clientTest ClientTest
148	data, err := ioutil.ReadAll(request.Body)
149	if err != nil {
150		http.Error(writer, "Could not read the test record.", http.StatusBadRequest)
151		return fmt.Errorf("Error for connection %s while reading test: %+v", request.RemoteAddr, err)
152	}
153	// check to make sure properly formatted client response
154	err = json.Unmarshal(data, &clientTest)
155	if err != nil {
156		http.Error(writer, "Could not parse the test record.", http.StatusBadRequest)
157		return fmt.Errorf("Error for user %s while parsing test: %+v", clientTest.Username, err)
158	}
159	log.Printf("Received test from user: %s at %s\tvia %s", clientTest.Username, request.RemoteAddr, request.UserAgent())
160	validateScore(&clientTest)
161	if err = writeTestFile(clientTest, data); err != nil {
162		http.Error(writer, "Could not save the test record.", http.StatusInternalServerError)
163		return fmt.Errorf("Error for user %s while writing test: %+v", clientTest.Username, err)
164	} else {
165		writer.WriteHeader(http.StatusCreated)
166		fmt.Fprint(writer, "Test received successfully.")
167	}
168	return err
169}
170
171// handleTestRequests creates an array of records for the client
172// the requestion expects 1 parameter (numQuestions)
173func handleTestQueries(writer http.ResponseWriter, request *http.Request) {
174	defer request.Body.Close()
175	if request.URL.Path != handlers[API_ROOT+"/test"].Request {
176		return
177	}
178	switch request.Method {
179	case "POST":
180		log.Printf("Client posting test: %+v\tvia %+v", request.RemoteAddr, request.UserAgent())
181		if err := handlePostingTest(writer, request); err != nil {
182			log.Printf("Error while receiving test: %+v", err)
183		}
184	case "GET":
185		log.Printf("Client requested test: %+v\tvia %+v", request.RemoteAddr, request.UserAgent())
186		if err := handleRequestForTest(writer, request); err != nil {
187			log.Printf("Error while serving test: %+v", err)
188		}
189
190	}
191}
192
193// handleRequestForCategories returns the JSON-encoded array
194// of categories available on the server
195func handleRequestForCategories(writer http.ResponseWriter, request *http.Request) error {
196	data, err := json.Marshal(categories)
197	if err != nil {
198		return err
199	}
200	_, err = writer.Write(data)
201	return err
202}
203
204// handleCategoryQueries returns the JSON-encoded array of categories available
205// to the requesting body
206func handleCategoryQueries(writer http.ResponseWriter, request *http.Request) {
207	defer request.Body.Close()
208	switch request.Method {
209	case "POST":
210		log.Printf("Client posting category: %+v\tvia %+v", request.RemoteAddr, request.UserAgent())
211	case "GET":
212		log.Printf("Client requested category: %+v\tvia %+v", request.RemoteAddr, request.UserAgent())
213		if err := handleRequestForCategories(writer, request); err != nil {
214			log.Printf("Error for client %+v: %+v", request.RemoteAddr, err)
215		}
216	}
217}
218
219// loadTestFile loads a user's test
220// in a parallel-safe way
221func loadTestFile(file *os.File, username string) error {
222	buffer, err := ioutil.ReadAll(file)
223	if err != nil {
224		return fmt.Errorf("Could not read test file: %+v\n", err.Error())
225	}
226	var test ClientTest
227	err = json.Unmarshal(buffer, &test)
228	if err != nil {
229		return fmt.Errorf("Could not parse test file %s: %+v\n", file.Name(), err.Error())
230	}
231	userTestsLock.Lock()
232	defer userTestsLock.Unlock()
233	if userTests[username] == nil {
234		userTests[username] = []ClientTest{}
235	}
236	userTests[username] = append(userTests[username], test)
237	return nil
238}
239
240// validateUserFile validates the existence of a user's test before attempting
241// to load it
242func validateUserFile(path string, fileInfo os.FileInfo, err error) error {
243	if !strings.Contains(path, serverConfig.USER_TESTS) {
244		return fmt.Errorf("Suspected malicious file access attempt: %s", path)
245	}
246	file, err := os.Open(path)
247	if err != nil {
248		return err
249	}
250	defer file.Close()
251	fileStats, err := file.Stat()
252	if err != nil {
253		return err
254	}
255	if fileStats.Mode().IsRegular() {
256		directories := strings.Split(filepath.Dir(path), string(filepath.Separator))
257		username := directories[len(directories)-1]
258		err = loadTestFile(file, username)
259		if err != nil {
260			return err
261		}
262	}
263	return nil
264}
265
266// handleScoreRequests gives the client the scores for the specified user name
267func handleScoreRequests(writer http.ResponseWriter, request *http.Request) {
268	defer request.Body.Close()
269	switch request.Method {
270	case "POST":
271		log.Printf("Client attempted to POST to /%s/test/score: %+v\tvia %+v", API_ROOT, request.RemoteAddr, request.UserAgent())
272	case "GET":
273		log.Printf("Client requested test score: %+v\tvia %+v", request.RemoteAddr, request.UserAgent())
274		username := request.FormValue("username")
275		if username != "" {
276			var err error
277			resultsFilePath := filepath.Join(serverConfig.USER_TESTS, username)
278			if err = filepath.Walk(resultsFilePath, validateUserFile); err != nil {
279				log.Printf("Failed to load user test due to error: %+v", err)
280				http.Error(writer, "Failed to load tests.", http.StatusInternalServerError)
281				return
282			}
283			userTestsLock.Lock()
284			defer userTestsLock.Unlock()
285			data, err := json.Marshal(userTests[username])
286			if err != nil {
287				log.Printf("Error for %s while marshaling: %+v", err, resultsFilePath)
288			}
289			writer.Write(data)
290			delete(userTests, username)
291		} else {
292			http.Error(writer, "No user specified.", http.StatusBadRequest)
293		}
294	}
295}
296
297// parseReceivedQuestions takes the questions from the client submission
298// and adds them to the database. The server expects a JSON-formatted array
299// of Records. There should be a TOKEN in the url. This TOKEN is a six-digit,
300// TOTP token based on SUBMISSION_SECRET.
301func parseReceivedQuestions(writer http.ResponseWriter, request *http.Request) {
302	token := request.FormValue("token")
303	correctToken, err := totp.GenerateCode(base32.StdEncoding.EncodeToString([]byte(SUBMISSION_SECRET)),
304		time.Now())
305	if err != nil {
306		log.Printf("Error generating token: %+v", err)
307	}
308	if token != correctToken {
309		http.Error(writer, "Invalid token.", http.StatusForbidden)
310		log.Printf("Invalid token from: %+v\tvia %+v\tis: %s, should be: %s", request.RemoteAddr,
311			request.UserAgent(), token, correctToken)
312		return
313	}
314	var newQuestions []Record
315	data, err := ioutil.ReadAll(request.Body)
316	if err != nil {
317		http.Error(writer, "Could not read the question submission.", http.StatusBadRequest)
318		log.Printf("Error for connection %s while reading questions: %+v", request.RemoteAddr, err)
319		return
320	}
321	// check to make sure properly formatted client response
322	err = json.Unmarshal(data, &newQuestions)
323	if err != nil {
324		http.Error(writer, "Could not parse the question submission.", http.StatusBadRequest)
325		log.Printf("Error while parsing submitted questions: %+v", err)
326		return
327	}
328	for _, record := range newQuestions {
329		err = addRecordToDB(&record)
330		if err != nil {
331			log.Printf("Could not add\n\t%+v\nto DB due to: %+v", err)
332		}
333	}
334}
335
336// handleQuestionQueries handles the submissions for questions
337func handleQuestionQueries(writer http.ResponseWriter, request *http.Request) {
338	defer request.Body.Close()
339	switch request.Method {
340	case "POST":
341		log.Printf("Client attempted to POST to %s/questions: %+v\tvia %+v", API_ROOT, request.RemoteAddr, request.UserAgent())
342		parseReceivedQuestions(writer, request)
343	case "GET":
344		log.Printf("Client attempted to GET from %s/questions: %+v\tvia %+v", API_ROOT, request.RemoteAddr, request.UserAgent())
345	}
346}
347
348type ServerHandler struct {
349	Request        string
350	HandleFunction func(writer http.ResponseWriter, request *http.Request)
351}
352
353var handlers map[string]ServerHandler
354
355func init() {
356	handlers = map[string]ServerHandler{
357		API_ROOT + "/test": ServerHandler{
358			Request:        API_ROOT + "/test",
359			HandleFunction: handleTestQueries},
360		API_ROOT + "/test/score": ServerHandler{
361			Request:        API_ROOT + "/test/score",
362			HandleFunction: handleScoreRequests},
363		API_ROOT + "/questions/categories": ServerHandler{
364			Request:        API_ROOT + "/questions/categories",
365			HandleFunction: handleCategoryQueries},
366		API_ROOT + "/questions": ServerHandler{
367			Request:        API_ROOT + "/questions",
368			HandleFunction: handleQuestionQueries},
369	}
370	userTests = map[string][]ClientTest{}
371}
372
373//TODO: ensure category cannot contain regex operators