package httpserver import ( "encoding/json" "fmt" "log" "net/http" "strings" "sync" "time" "github.com/AFASystems/presence/internal/pkg/model" "github.com/AFASystems/presence/internal/pkg/persistence" "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/gorilla/websocket" ) var ( upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } ) const ( writeWait = 10 * time.Second pongWait = 60 * time.Second pingPeriod = (pongWait * 9) / 10 beaconPeriod = 2 * time.Second ) // Init store in main or anywhere else and pass it to all initializer functions // called in main, then with controllers or handlers use wrapper that takes entire store // allocates only the properties that need to be passed into the controller func StartHTTPServer(addr string, ctx *model.AppContext) { headersOk := handlers.AllowedHeaders([]string{"X-Requested-With"}) originsOk := handlers.AllowedOrigins([]string{"*"}) methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}) // Set up HTTP server r := mux.NewRouter() r.HandleFunc("/api/results", resultsHandler(&ctx.HTTPResults)) r.HandleFunc("/api/beacons/{beacon_id}", BeaconsDeleteHandler(&ctx.Beacons, ctx.ButtonsList)).Methods("DELETE") r.HandleFunc("/api/beacons", BeaconsListHandler(&ctx.Beacons)).Methods("GET") r.HandleFunc("/api/beacons", BeaconsAddHandler(&ctx.Beacons)).Methods("POST") //since beacons are hashmap, just have put and post be same thing. it'll either add or modify that entry r.HandleFunc("/api/beacons", BeaconsAddHandler(&ctx.Beacons)).Methods("PUT") r.HandleFunc("/api/latest-beacons", latestBeaconsListHandler(&ctx.LatestList)).Methods("GET") r.HandleFunc("/api/settings", SettingsListHandler(&ctx.Settings)).Methods("GET") r.HandleFunc("/api/settings", SettingsEditHandler(&ctx.Settings)).Methods("POST") r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(http.Dir("static_html/js/")))) r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir("static_html/css/")))) r.PathPrefix("/img/").Handler(http.StripPrefix("/img/", http.FileServer(http.Dir("static_html/img/")))) r.PathPrefix("/").Handler(http.FileServer(http.Dir("static_html/"))) http.Handle("/", r) mxWS := mux.NewRouter() mxWS.HandleFunc("/ws/api/beacons", serveWs(&ctx.HTTPResults)) mxWS.HandleFunc("/ws/api/beacons/latest", serveLatestBeaconsWs(&ctx.LatestList)) mxWS.HandleFunc("/ws/broadcast", handleConnections(ctx.Clients, &ctx.Broadcast)) http.Handle("/ws/", mxWS) go handleMessages(ctx.Clients, &ctx.Broadcast) http.ListenAndServe(addr, handlers.CORS(originsOk, headersOk, methodsOk)(r)) } func resultsHandler(httpResults *model.HTTPResultsList) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { httpResults.HTTPResultsLock.Lock() defer httpResults.HTTPResultsLock.Unlock() js, err := json.Marshal(httpResults.HTTPResults) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write(js) } } func BeaconsListHandler(beacons *model.BeaconsList) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { beacons.Lock.RLock() js, err := json.Marshal(beacons.Beacons) beacons.Lock.RUnlock() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write(js) } } func BeaconsAddHandler(beacons *model.BeaconsList) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) var inBeacon model.Beacon err := decoder.Decode(&inBeacon) if err != nil { http.Error(w, err.Error(), 400) return } if (len(strings.TrimSpace(inBeacon.Name)) == 0) || (len(strings.TrimSpace(inBeacon.Beacon_id)) == 0) { http.Error(w, "name and beacon_id cannot be blank", 400) return } beacons.Beacons[inBeacon.Beacon_id] = inBeacon err = persistence.PersistBeacons(beacons) if err != nil { http.Error(w, "trouble persisting beacons list, create bucket", 500) return } w.Write([]byte("ok")) } } func BeaconsDeleteHandler(beacons *model.BeaconsList, buttonsList map[string]model.Button) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) fmt.Println("route param: ", vars) beaconId := vars["beacon_id"] _, ok := beacons.Beacons[beaconId] if !ok { http.Error(w, "no beacon with the specified id", 400) // change the status code return } delete(beacons.Beacons, beaconId) _, ok = buttonsList[beaconId] if ok { delete(buttonsList, beaconId) } err := persistence.PersistBeacons(beacons) if err != nil { http.Error(w, "trouble persisting beacons list, create bucket", 500) return } w.Write([]byte("ok")) } } func latestBeaconsListHandler(latestList *model.LatestBeaconsList) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { latestList.LatestListLock.RLock() var la = make([]model.Beacon, 0) for _, b := range latestList.LatestList { la = append(la, b) } latestList.LatestListLock.RUnlock() js, err := json.Marshal(la) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write(js) } } func SettingsListHandler(settings *model.Settings) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { js, err := json.Marshal(settings) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Write(js) } } func SettingsEditHandler(settings *model.Settings) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) var inSettings model.Settings err := decoder.Decode(&inSettings) if err != nil { http.Error(w, err.Error(), 400) return } //make sure values are > 0 if (inSettings.Location_confidence <= 0) || (inSettings.Last_seen_threshold <= 0) || (inSettings.HA_send_interval <= 0) { http.Error(w, "values must be greater than 0", 400) return } *settings = inSettings err = persistence.PersistSettings(settings) if err != nil { http.Error(w, "trouble persisting settings, create bucket", 500) return } w.Write([]byte("ok")) } } func reader(ws *websocket.Conn) { defer ws.Close() ws.SetReadLimit(512) ws.SetReadDeadline(time.Now().Add(pongWait)) ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) for { _, _, err := ws.ReadMessage() if err != nil { break } } } func writer(ws *websocket.Conn, httpResult *model.HTTPResultsList) { pingTicker := time.NewTicker(pingPeriod) beaconTicker := time.NewTicker(beaconPeriod) defer func() { pingTicker.Stop() beaconTicker.Stop() ws.Close() }() for { select { case <-beaconTicker.C: httpResult.HTTPResultsLock.Lock() defer httpResult.HTTPResultsLock.Unlock() js, err := json.Marshal(httpResult.HTTPResults) if err != nil { js = []byte("error") } ws.SetWriteDeadline(time.Now().Add(writeWait)) if err := ws.WriteMessage(websocket.TextMessage, js); err != nil { return } case <-pingTicker.C: ws.SetWriteDeadline(time.Now().Add(writeWait)) if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { return } } } } func serveWs(httpResult *model.HTTPResultsList) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ws, err := upgrader.Upgrade(w, r, nil) if err != nil { if _, ok := err.(websocket.HandshakeError); !ok { log.Println(err) } return } go writer(ws, httpResult) reader(ws) } } func latestBeaconWriter(ws *websocket.Conn, latestBeaconsList map[string]model.Beacon, lock *sync.RWMutex) { pingTicker := time.NewTicker(pingPeriod) beaconTicker := time.NewTicker(beaconPeriod) defer func() { pingTicker.Stop() beaconTicker.Stop() ws.Close() }() for { select { case <-beaconTicker.C: lock.RLock() var la = make([]model.Beacon, 0) for _, b := range latestBeaconsList { la = append(la, b) } lock.RUnlock() js, err := json.Marshal(la) if err != nil { js = []byte("error") } ws.SetWriteDeadline(time.Now().Add(writeWait)) if err := ws.WriteMessage(websocket.TextMessage, js); err != nil { return } case <-pingTicker.C: ws.SetWriteDeadline(time.Now().Add(writeWait)) if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { return } } } } func serveLatestBeaconsWs(latestList *model.LatestBeaconsList) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ws, err := upgrader.Upgrade(w, r, nil) if err != nil { if _, ok := err.(websocket.HandshakeError); !ok { log.Println(err) } return } go latestBeaconWriter(ws, latestList.LatestList, &latestList.LatestListLock) reader(ws) } } func handleConnections(clients map[*websocket.Conn]bool, broadcast *chan model.Message) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ws, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Fatal(err) } defer ws.Close() clients[ws] = true for { var msg model.Message err := ws.ReadJSON(&msg) if err != nil { log.Printf("error: %v", err) delete(clients, ws) break } *broadcast <- msg } } } func handleMessages(clients map[*websocket.Conn]bool, broadcast *chan model.Message) { for { msg := <-*broadcast for client := range clients { err := client.WriteJSON(msg) if err != nil { log.Printf("error: %v", err) client.Close() delete(clients, client) } } } }