//ValidateEventTrackingPayloadHandler validates that the payload has a valid JSON Schema //Uses a FinalHandler as next because it consumes the request's body func ValidateEventTrackingPayloadHandler(next *ctx.FinalHandler) *ctx.Handler { return ctx.NewHandler(next.Context, func(c *ctx.Context, w http.ResponseWriter, r *http.Request) { //Validate the payload body, err := ioutil.ReadAll(r.Body) if err != nil { c.Logger.Errorf("%s : Error reading body: %s", utilities.GetIP(r), err.Error()) http.Error(w, http.StatusText(500), 500) return } next.Payload = body requestLoader := gojsonschema.NewStringLoader(string(body)) result, err := c.JSONTrackingEventValidator.Schema.Validate(requestLoader) if err != nil { c.Logger.Infof("%s : Json validation error: %s", utilities.GetIP(r), err.Error()) http.Error(w, http.StatusText(400), 400) return } if !result.Valid() { c.Logger.Infof("%s : Payload is not valid: %+v", utilities.GetIP(r), result.Errors()) http.Error(w, http.StatusText(400), 400) return } next.ServeHTTP(w, r) }) }
//AuthHandler middleware //This handler handles auth based on the assertion that the request is valid JSON //Verifies for access, blocks handlers chain if access denied func AuthHandler(next *ctx.Handler) *ctx.Handler { return ctx.NewHandler(next.Context, func(c *ctx.Context, w http.ResponseWriter, r *http.Request) { //TODO: This can change for body instead ? token := r.Header.Get("Tracking-Token") //Bad request token empty or not present if token == "" { c.Logger.Infof("%s : No token header", utilities.GetIP(r)) token = r.URL.Query().Get("key") if token == "" { c.Logger.Infof("%s : No token query parameter", utilities.GetIP(r)) http.Error(w, http.StatusText(400), 400) return } } authorized, err := c.AuthDb.IsTokenAuthorized(token) // Internal server error TODO: Handle this if err != nil { c.Logger.Errorf("%s : Error while authorizing: %s", utilities.GetIP(r), err.Error()) http.Error(w, http.StatusText(500), 500) return } // Unauthorized if !authorized { c.Logger.Warnf("%s : Unauthorized: %s", utilities.GetIP(r), token) http.Error(w, http.StatusText(401), 401) return } c.Logger.Infof("%s : Authorized", utilities.GetIP(r)) next.ServeHTTP(w, r) }) }
//EnforceJSONHandler middleware //This handler can handle raw requests //This handler checks for detected content type as well as content-type header func EnforceJSONHandler(next *ctx.Handler) *ctx.Handler { return ctx.NewHandler(next.Context, func(c *ctx.Context, w http.ResponseWriter, r *http.Request) { //Ensure that there is a body if r.ContentLength == 0 { c.Logger.Infof("%s : Recieved empty payload", utilities.GetIP(r)) http.Error(w, http.StatusText(400), 400) return } //Ensure that its json contentType := r.Header.Get("Content-Type") if contentType != "application/json; charset=UTF-8" { c.Logger.Infof("%s : Invalid content type. Got %s", utilities.GetIP(r), contentType) http.Error(w, http.StatusText(415), 415) return } next.ServeHTTP(w, r) }) }
//Handler for /track route //From here we have a valid authentified json request with correct schema func handleTrack(c *ctx.Context, p []byte, w http.ResponseWriter, r *http.Request) { err := c.Queue.PublishEventsTrackingTask(p) if err != nil { //TODO : Could we handle this a little better? c.Logger.Errorf("Error publishing to queue: %s\nPayload: %s", err.Error(), string(p)) http.Error(w, http.StatusText(500), 500) } c.Logger.Infof("%s : Valid payload recieved and treated", utilities.GetIP(r)) }
//Handler for /connected route //Allows websocket connection //From here we have an authentified request func handleConnected(c *ctx.Context, w http.ResponseWriter, r *http.Request) { const pongWait time.Duration = 10 * time.Second const writeWait time.Duration = 10 * time.Second conn, err := upgrader.Upgrade(w, r, nil) if err != nil { //TODO : Could we handle this a little better? c.Logger.Errorf("%s : Could not upgrade to websocket protocol: %s", utilities.GetIP(r), err.Error()) http.Error(w, http.StatusText(500), 500) return } //We defer the closing of the connection defer conn.Close() //Custom settings for timeouts conn.SetReadDeadline(time.Now().Add(pongWait)) conn.SetPongHandler(func(string) error { conn.SetReadDeadline(time.Now().Add(pongWait)) return nil }) for { messageType, payload, err := conn.ReadMessage() if err != nil { if neterr, ok := err.(net.Error); ok && neterr.Timeout() { c.Logger.Infof("%s : Connection timeout: %s", utilities.GetIP(r), err.Error()) err = conn.WriteControl(websocket.CloseMessage, []byte("Timeout"), time.Now().Add(writeWait)) return } c.Logger.Infof("%s : Error reading message: %s", utilities.GetIP(r), err.Error()) err = conn.WriteControl(websocket.CloseInvalidFramePayloadData, []byte("Cannot read message"), time.Now().Add(writeWait)) return } if messageType != websocket.TextMessage { c.Logger.Infof("%s : Unhandled message type : %d", utilities.GetIP(r), messageType) err = conn.WriteControl(websocket.CloseUnsupportedData, []byte("Unhandled message type"), time.Now().Add(writeWait)) return } //We need to validate the payload and then send it payloadLoader := gojsonschema.NewStringLoader(string(payload)) result, err := c.JSONTrackingEventValidator.Schema.Validate(payloadLoader) if err != nil { c.Logger.Infof("%s : Json validation error: %s", utilities.GetIP(r), err.Error()) err = conn.WriteControl(websocket.CloseUnsupportedData, []byte("Json validation error"), time.Now().Add(writeWait)) return } if !result.Valid() { c.Logger.Infof("%s : Payload is not valid: %+v", utilities.GetIP(r), result.Errors()) conn.SetWriteDeadline(time.Now().Add(writeWait)) err = conn.WriteJSON(&wsResponse{"ERROR", "Invalid payload"}) return } err = c.Queue.PublishEventsTrackingTask(payload) if err != nil { //TODO : Could we handle this a little better? c.Logger.Errorf("%s : Error publishing to queue: %s\nPayload: %s", utilities.GetIP(r), err.Error(), string(payload)) http.Error(w, http.StatusText(500), 500) } //TODO: ack the request c.Logger.Infof("%s : Valid payload recieved and treated", utilities.GetIP(r)) conn.SetWriteDeadline(time.Now().Add(writeWait)) conn.WriteJSON(&wsResponse{"OK", ""}) } }