master
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